release-skill 0.8.1 → 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 (126) 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 +63 -0
  7. package/INSTALL.md +27 -2
  8. package/INSTALL.zh-CN.md +19 -2
  9. package/README.md +41 -22
  10. package/README.zh-CN.md +34 -20
  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-local-finish.mjs +4 -0
  18. package/adapters/claude/bin/release-skill.bundle.mjs +3955 -2431
  19. package/adapters/claude/bin/rules.json +1 -1
  20. package/adapters/claude/bin/schemas/consumer-contract-vector.schema.json +149 -0
  21. package/adapters/claude/schemas/release-plan.schema.json +40 -3
  22. package/adapters/claude/skills/release-finish/SKILL.md +65 -0
  23. package/adapters/claude/skills/release-help/SKILL.md +3 -2
  24. package/adapters/claude/skills/release-prepare/SKILL.md +2 -2
  25. package/adapters/claude/skills/release-publish/SKILL.md +3 -1
  26. package/adapters/claude/skills/release-verify/SKILL.md +6 -1
  27. package/adapters/claude/src/schemas/migration-manifest.schema.json +271 -7
  28. package/adapters/claude/src/schemas/profile-adoption-declaration.schema.json +8 -0
  29. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  30. package/adapters/codex/bin/consumer-contract-vectors.json +110 -0
  31. package/adapters/codex/bin/error-codes.json +8 -1
  32. package/adapters/codex/bin/foundation-resource-binding.json +1 -1
  33. package/adapters/codex/bin/registry.json +1 -1
  34. package/adapters/codex/bin/release-skill-local-finish.mjs +4 -0
  35. package/adapters/codex/bin/release-skill.bundle.mjs +3955 -2431
  36. package/adapters/codex/bin/rules.json +1 -1
  37. package/adapters/codex/bin/schemas/consumer-contract-vector.schema.json +149 -0
  38. package/adapters/codex/schemas/release-plan.schema.json +40 -3
  39. package/adapters/codex/skills/release-finish/SKILL.md +72 -0
  40. package/adapters/codex/skills/release-help/SKILL.md +3 -2
  41. package/adapters/codex/skills/release-prepare/SKILL.md +2 -2
  42. package/adapters/codex/skills/release-publish/SKILL.md +3 -1
  43. package/adapters/codex/skills/release-verify/SKILL.md +6 -1
  44. package/adapters/codex/src/schemas/migration-manifest.schema.json +271 -7
  45. package/adapters/codex/src/schemas/profile-adoption-declaration.schema.json +8 -0
  46. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  47. package/adapters/kimi/bin/consumer-contract-vectors.json +110 -0
  48. package/adapters/kimi/bin/error-codes.json +8 -1
  49. package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
  50. package/adapters/kimi/bin/registry.json +1 -1
  51. package/adapters/kimi/bin/release-skill-local-finish.mjs +4 -0
  52. package/adapters/kimi/bin/release-skill.bundle.mjs +3955 -2431
  53. package/adapters/kimi/bin/rules.json +1 -1
  54. package/adapters/kimi/bin/schemas/consumer-contract-vector.schema.json +149 -0
  55. package/adapters/kimi/schemas/release-plan.schema.json +40 -3
  56. package/adapters/kimi/skills/release-finish/SKILL.md +72 -0
  57. package/adapters/kimi/skills/release-help/SKILL.md +3 -2
  58. package/adapters/kimi/skills/release-prepare/SKILL.md +2 -2
  59. package/adapters/kimi/skills/release-publish/SKILL.md +3 -1
  60. package/adapters/kimi/skills/release-verify/SKILL.md +6 -1
  61. package/adapters/kimi/src/schemas/migration-manifest.schema.json +271 -7
  62. package/adapters/kimi/src/schemas/profile-adoption-declaration.schema.json +8 -0
  63. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  64. package/adapters/workbuddy/bin/consumer-contract-vectors.json +110 -0
  65. package/adapters/workbuddy/bin/error-codes.json +8 -1
  66. package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
  67. package/adapters/workbuddy/bin/registry.json +1 -1
  68. package/adapters/workbuddy/bin/release-skill-local-finish.mjs +4 -0
  69. package/adapters/workbuddy/bin/release-skill.bundle.mjs +3955 -2431
  70. package/adapters/workbuddy/bin/rules.json +1 -1
  71. package/adapters/workbuddy/bin/schemas/consumer-contract-vector.schema.json +149 -0
  72. package/adapters/workbuddy/schemas/release-plan.schema.json +40 -3
  73. package/adapters/workbuddy/skills/release-finish/SKILL.md +65 -0
  74. package/adapters/workbuddy/skills/release-help/SKILL.md +3 -2
  75. package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -2
  76. package/adapters/workbuddy/skills/release-publish/SKILL.md +3 -1
  77. package/adapters/workbuddy/skills/release-verify/SKILL.md +6 -1
  78. package/adapters/workbuddy/src/schemas/migration-manifest.schema.json +271 -7
  79. package/adapters/workbuddy/src/schemas/profile-adoption-declaration.schema.json +8 -0
  80. package/bin/consumer-contract-vectors.json +110 -0
  81. package/bin/error-codes.json +8 -1
  82. package/bin/foundation-resource-binding.json +1 -1
  83. package/bin/registry.json +1 -1
  84. package/bin/release-skill-cli.mjs +163 -2
  85. package/bin/release-skill-local-finish.mjs +4 -0
  86. package/bin/release-skill.bundle.mjs +3955 -2431
  87. package/bin/rules.json +1 -1
  88. package/bin/schemas/consumer-contract-vector.schema.json +149 -0
  89. package/package.json +4 -4
  90. package/platform-manifest.json +31 -5
  91. package/schemas/release-plan.schema.json +40 -3
  92. package/skills/release-finish/SKILL.md +65 -0
  93. package/skills/release-help/SKILL.md +3 -2
  94. package/skills/release-prepare/SKILL.md +2 -2
  95. package/skills/release-publish/SKILL.md +3 -1
  96. package/skills/release-verify/SKILL.md +6 -1
  97. package/skills-src/release-finish/SKILL.md +65 -0
  98. package/skills-src/release-help/SKILL.md +3 -2
  99. package/skills-src/release-prepare/SKILL.md +2 -2
  100. package/skills-src/release-publish/SKILL.md +3 -1
  101. package/skills-src/release-verify/SKILL.md +6 -1
  102. package/src/commands/approve.mjs +14 -2
  103. package/src/commands/distribute.mjs +562 -321
  104. package/src/commands/post-release-local.mjs +802 -0
  105. package/src/commands/postverify.mjs +262 -155
  106. package/src/commands/prepare.mjs +106 -56
  107. package/src/commands/publish.mjs +11 -10
  108. package/src/commands/reconcile.mjs +11 -10
  109. package/src/commands/setup.mjs +26 -13
  110. package/src/commands/ship.mjs +169 -28
  111. package/src/commands/verify.mjs +19 -8
  112. package/src/core/adoption-assessment.mjs +0 -3
  113. package/src/core/approval.mjs +11 -8
  114. package/src/core/derived-artifact-gates.mjs +32 -5
  115. package/src/core/hooks.mjs +0 -16
  116. package/src/core/plan.mjs +19 -11
  117. package/src/core/postpublish-approval.mjs +12 -2
  118. package/src/core/postpublish-bundle.mjs +51 -13
  119. package/src/core/postpublish.mjs +146 -6
  120. package/src/core/preset-executor.mjs +53 -0
  121. package/src/core/proposal-inbox.mjs +91 -27
  122. package/src/core/recovery.mjs +27 -12
  123. package/src/producers/build-adapters.mjs +10 -1
  124. package/src/producers/foundation-resource-projection.mjs +16 -3
  125. package/src/schemas/migration-manifest.schema.json +271 -7
  126. package/src/schemas/profile-adoption-declaration.schema.json +8 -0
@@ -0,0 +1,802 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join, relative } from 'node:path';
4
+
5
+ import {
6
+ readFileContained,
7
+ resolveContained,
8
+ superviseProcess,
9
+ withTemporaryWorkspace,
10
+ } from 'skill-family-harness-node';
11
+
12
+ import { getPlatform } from '../platforms/registry.mjs';
13
+ import { resolveCodeBuddyMarketplace } from '../platforms/codebuddy.mjs';
14
+ import { verifyInstalledMarketplacePayload } from '../adapters/plugin-marketplace.mjs';
15
+
16
+ const HOSTS_BY_ACTION = Object.freeze({
17
+ 'claude-marketplace-install': ['claude'],
18
+ 'codex-marketplace-install': ['codex'],
19
+ 'kimi-marketplace-install': ['kimi'],
20
+ 'codebuddy-marketplace-install': ['codebuddy', 'workbuddy'],
21
+ });
22
+
23
+ const DISTRIBUTION_BY_ACTION = Object.freeze({
24
+ 'claude-marketplace-install': 'claude-plugin',
25
+ 'codex-marketplace-install': 'codex-plugin',
26
+ 'kimi-marketplace-install': 'kimi-plugin',
27
+ 'codebuddy-marketplace-install': 'codebuddy-plugin',
28
+ });
29
+
30
+ const BRANCH_ACTION_INCLUDED = new Set(['advance-existing-branch', 'initialize-default-branch']);
31
+ const CODEBUDDY_MACOS_PATH = '/Applications/WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy';
32
+ const SAFE_ENV_KEYS = Object.freeze([
33
+ 'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'TMPDIR',
34
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
35
+ 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
36
+ 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CONFIG_DIR',
37
+ ]);
38
+
39
+ function actionTarget(plan, action, host) {
40
+ const parameters = action.parameters ?? {};
41
+ const sourceDescriptor = parameters.sourceDescriptor ?? {};
42
+ const unit = (plan.units ?? []).find((candidate) => candidate.id === action.unitId);
43
+ if (!unit) throw new Error(`post-release action ${action.id} references an unknown unit`);
44
+ const distributionType = DISTRIBUTION_BY_ACTION[action.type];
45
+ const distribution = (unit.distributions ?? []).find((candidate) => (
46
+ candidate.type === distributionType && candidate.plugin === parameters.plugin
47
+ ));
48
+ if (!distribution) {
49
+ throw new Error(`post-release action ${action.id} is not backed by its unit distribution`);
50
+ }
51
+ if (unit.targetVersion !== parameters.version) {
52
+ throw new Error(`post-release action ${action.id} disagrees with its frozen unit identity`);
53
+ }
54
+ const pluginRepo = sourceDescriptor.form === 'standalone-index'
55
+ ? sourceDescriptor.pluginRepo
56
+ : sourceDescriptor.repo;
57
+ const marketplaceRepo = sourceDescriptor.form === 'standalone-index'
58
+ ? sourceDescriptor.marketplaceRepo
59
+ : sourceDescriptor.repo;
60
+ const pluginCommit = parameters.sourceCommit;
61
+ const marketplaceCommit = sourceDescriptor.form === 'standalone-index'
62
+ ? parameters.marketplaceCommitSha
63
+ : pluginCommit;
64
+ const tagAction = (plan.externalActions ?? []).find((candidate) => (
65
+ candidate.type === 'create-tag' && candidate.unitId === action.unitId
66
+ ));
67
+ const pushAction = (plan.externalActions ?? []).find((candidate) => (
68
+ candidate.type === 'push-snapshot' && candidate.unitId === action.unitId
69
+ ));
70
+ const pluginTag = tagAction?.parameters?.tag;
71
+ if (
72
+ pluginRepo !== unit.publicRepo
73
+ || marketplaceRepo !== parameters.repo
74
+ || sourceDescriptor.marketplaceEntry !== parameters.plugin
75
+ || tagAction?.parameters?.repo !== pluginRepo
76
+ || tagAction?.parameters?.commit !== pluginCommit
77
+ || tagAction?.parameters?.version !== parameters.version
78
+ ) {
79
+ throw new Error(`post-release action ${action.id} disagrees with its frozen source identities`);
80
+ }
81
+ if (sourceDescriptor.form === 'standalone-index') {
82
+ if (
83
+ sourceDescriptor.marketplaceCommitSha !== marketplaceCommit
84
+ || sourceDescriptor.ref !== parameters.ref
85
+ ) {
86
+ throw new Error(`post-release action ${action.id} disagrees with its standalone marketplace identity`);
87
+ }
88
+ } else if (sourceDescriptor.form !== 'bundled-family' || sourceDescriptor.commit !== pluginCommit) {
89
+ throw new Error(`post-release action ${action.id} has no single frozen source commit`);
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;
113
+ return {
114
+ host,
115
+ actionId: action.id,
116
+ actionType: action.type,
117
+ unitId: action.unitId,
118
+ plugin: parameters.plugin,
119
+ marketplace: host === 'codebuddy' || host === 'workbuddy'
120
+ ? resolveCodeBuddyMarketplace(parameters)
121
+ : parameters.marketplace ?? sourceDescriptor.marketplaceEntry ?? parameters.plugin,
122
+ version: parameters.version,
123
+ pluginRepo,
124
+ githubHost: tagAction?.parameters?.githubHost ?? pushAction?.parameters?.githubHost ?? 'github.com',
125
+ pluginTag,
126
+ pluginCommit,
127
+ marketplaceRepo,
128
+ marketplaceRef,
129
+ marketplaceCommit,
130
+ sourceForm: sourceDescriptor.form,
131
+ timeoutMs: parameters.timeoutMs ?? 300_000,
132
+ };
133
+ }
134
+
135
+ function pluginTargets(plan) {
136
+ const targets = [];
137
+ for (const action of plan.externalActions ?? []) {
138
+ const hosts = HOSTS_BY_ACTION[action.type];
139
+ if (!hosts) continue;
140
+ for (const host of hosts) targets.push(actionTarget(plan, action, host));
141
+ }
142
+ return targets.sort((left, right) => (
143
+ left.host.localeCompare(right.host) || left.unitId.localeCompare(right.unitId)
144
+ ));
145
+ }
146
+
147
+ function assertExecutableTarget(target) {
148
+ for (const field of [
149
+ 'actionId', 'unitId', 'plugin', 'version',
150
+ 'pluginRepo', 'pluginTag', 'pluginCommit',
151
+ 'marketplaceRepo', 'marketplaceRef', 'marketplaceCommit',
152
+ ]) {
153
+ if (typeof target[field] !== 'string' || target[field].length === 0) {
154
+ throw new Error(`local host update target is missing ${field}`);
155
+ }
156
+ }
157
+ if (!/^[a-f0-9]{40}$/u.test(target.pluginCommit) || !/^[a-f0-9]{40}$/u.test(target.marketplaceCommit)) {
158
+ throw new Error(`local host update requires the frozen public commit for unit ${target.unitId}`);
159
+ }
160
+ }
161
+
162
+ export function derivePostReleaseChecklist(plan) {
163
+ if (!plan || typeof plan !== 'object' || typeof plan.digest !== 'string') {
164
+ throw new Error('a frozen release plan with digest is required');
165
+ }
166
+ const units = plan.units ?? [];
167
+ const uncovered = units.filter((unit) => (
168
+ !BRANCH_ACTION_INCLUDED.has(unit.productionConfig?.branchStrategy)
169
+ ));
170
+ const targets = pluginTargets(plan);
171
+ return {
172
+ command: 'post-release',
173
+ status: 'AWAITING_USER_DECISION',
174
+ planDigest: plan.digest,
175
+ merge: {
176
+ promptRequired: uncovered.length > 0,
177
+ alreadyHandledByRelease: uncovered.length === 0,
178
+ executionIncluded: false,
179
+ units: uncovered.map((unit) => ({
180
+ unitId: unit.id,
181
+ branchStrategy: unit.productionConfig?.branchStrategy ?? null,
182
+ publishedBranch: unit.frozenSnapshot?.branch ?? null,
183
+ })),
184
+ },
185
+ localHostUpdate: {
186
+ promptRequired: targets.length > 0,
187
+ available: targets.length > 0,
188
+ hosts: [...new Set(targets.map((target) => target.host))].sort(),
189
+ targets,
190
+ },
191
+ };
192
+ }
193
+
194
+ export function unavailablePostReleaseChecklist(plan, error) {
195
+ return {
196
+ command: 'post-release',
197
+ status: 'UNAVAILABLE',
198
+ planDigest: plan?.digest ?? null,
199
+ releaseStatusChanged: false,
200
+ diagnostic: {
201
+ code: 'POST_RELEASE_CHECKLIST_UNAVAILABLE',
202
+ message: error?.message ?? String(error),
203
+ },
204
+ };
205
+ }
206
+
207
+ export function assertVerifiedReleaseRun(plan, runRecord) {
208
+ if (
209
+ runRecord?.command !== 'verify'
210
+ || runRecord?.status !== 'VERIFIED'
211
+ || runRecord?.planDigest !== plan?.digest
212
+ ) {
213
+ throw new Error('post-release local work requires a VERIFIED run bound to the frozen plan');
214
+ }
215
+ }
216
+
217
+ function hostEnvironment(host, { kimiHome } = {}) {
218
+ const env = {};
219
+ for (const key of SAFE_ENV_KEYS) {
220
+ if (process.env[key] !== undefined) env[key] = process.env[key];
221
+ }
222
+ env.HOME ??= homedir();
223
+ env.PATH ??= '/usr/bin:/bin';
224
+ env.GIT_TERMINAL_PROMPT = '0';
225
+ if (host === 'workbuddy') env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
226
+ if (host === 'kimi' && kimiHome) env.KIMI_CONFIG_DIR = kimiHome;
227
+ return env;
228
+ }
229
+
230
+ async function defaultRun(command, args, options = {}) {
231
+ return withTemporaryWorkspace(async (workspace) => {
232
+ const stdoutFile = 'stdout.log';
233
+ const stderrFile = 'stderr.log';
234
+ const envelope = await superviseProcess({
235
+ command,
236
+ args,
237
+ cwd: options.cwd ?? process.cwd(),
238
+ env: options.env ?? hostEnvironment('generic'),
239
+ timeoutPolicy: {
240
+ maxSeconds: Math.max(1, Math.ceil((options.timeout ?? 120_000) / 1000)),
241
+ killGraceSeconds: 5,
242
+ },
243
+ rawSink: { root: workspace.root, stdoutFile, stderrFile },
244
+ outputByteLimits: { stdout: 8 * 1024 * 1024, stderr: 8 * 1024 * 1024 },
245
+ });
246
+ const [stdout, stderr] = await Promise.all([
247
+ workspace.readFile(stdoutFile, { encoding: 'utf8' }),
248
+ workspace.readFile(stderrFile, { encoding: 'utf8' }),
249
+ ]);
250
+ if (!envelope.ok) {
251
+ const error = new Error(
252
+ `${command} exited ${envelope.exitStatus}${stderr.trim() ? `: ${stderr.trim()}` : ''}`,
253
+ );
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
+ };
262
+ throw error;
263
+ }
264
+ return { stdout, stderr };
265
+ }, { prefix: 'release-skill-host-command-' });
266
+ }
267
+
268
+ async function commandAvailable(command, host, run) {
269
+ try {
270
+ await run(command, ['--version'], { timeout: 10_000, env: hostEnvironment(host) });
271
+ return true;
272
+ } catch (error) {
273
+ if (error?.hostCommandUnavailable === true) return false;
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ async function defaultDetect(host, run = defaultRun) {
279
+ if (host === 'codebuddy' || host === 'workbuddy') {
280
+ for (const command of ['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH]) {
281
+ if (await commandAvailable(command, host, run)) return { available: true, command };
282
+ }
283
+ return { available: false, reason: 'CodeBuddy/WorkBuddy CLI not found' };
284
+ }
285
+ if (host === 'kimi') {
286
+ if (!await commandAvailable('kimi', host, run)) return { available: false, reason: 'kimi CLI not found' };
287
+ try {
288
+ await access('/usr/bin/expect');
289
+ } catch {
290
+ return { available: false, reason: '/usr/bin/expect not found' };
291
+ }
292
+ return { available: true, command: 'kimi', expectCommand: '/usr/bin/expect' };
293
+ }
294
+ if (!await commandAvailable(host, host, run)) return { available: false, reason: `${host} CLI not found` };
295
+ return { available: true, command: host };
296
+ }
297
+
298
+ function parseJson(stdout, label) {
299
+ try {
300
+ return JSON.parse(stdout);
301
+ } catch {
302
+ throw new Error(`${label} did not return JSON`);
303
+ }
304
+ }
305
+
306
+ function exactPluginObservation(target, stdout) {
307
+ const selector = `${target.plugin}@${target.marketplace}`;
308
+ const parsed = parseJson(stdout, `${target.host} plugin list`);
309
+ if (target.host === 'claude' || target.host === 'codex') {
310
+ const platform = getPlatform(target.host);
311
+ const observed = platform.strategy.parseListOutput(parsed, selector);
312
+ if (!observed.ok) {
313
+ if (observed.error.includes('not found')) return { installed: false };
314
+ throw new Error(observed.error);
315
+ }
316
+ const identity = platform.strategy.extractListIdentity(observed.found);
317
+ if (
318
+ identity.plugin !== target.plugin
319
+ || identity.marketplace !== target.marketplace
320
+ || identity.version !== target.version
321
+ ) {
322
+ return { installed: true, exact: false, found: observed.found };
323
+ }
324
+ if (platform.strategy.crossValidateListEntry) {
325
+ const cross = platform.strategy.crossValidateListEntry(observed.found, target);
326
+ if (!cross.ok) return { installed: true, exact: false, found: observed.found };
327
+ }
328
+ return {
329
+ installed: true,
330
+ exact: true,
331
+ found: observed.found,
332
+ ...(observed.installPath ? { installPath: observed.installPath } : {}),
333
+ };
334
+ }
335
+ if (!Array.isArray(parsed)) throw new Error(`${target.host} plugin list did not return an array`);
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;
340
+ return {
341
+ installed: true,
342
+ exact: found.version === target.version && found.gitCommitSha === target.pluginCommit,
343
+ found,
344
+ };
345
+ }
346
+
347
+ function normalizeGitSource(source) {
348
+ return String(source ?? '')
349
+ .replace(/^git\+/, '')
350
+ .replace(/^https:\/\/github\.com\//, '')
351
+ .replace(/^git@github\.com:/, '')
352
+ .replace(/\.git$/u, '')
353
+ .replace(/\/$/u, '');
354
+ }
355
+
356
+ async function observeMarketplace(target, command, env, run) {
357
+ const listed = await run(command, ['plugin', 'marketplace', 'list', '--json'], { env });
358
+ const parsed = parseJson(listed.stdout, `${target.host} marketplace list`);
359
+ const entries = target.host === 'claude' ? parsed : parsed?.marketplaces;
360
+ if (!Array.isArray(entries)) throw new Error(`${target.host} marketplace list has an invalid shape`);
361
+ const found = entries.find((entry) => entry?.name === target.marketplace);
362
+ if (!found) return { installed: false };
363
+ const source = target.host === 'claude' ? found.repo : found.marketplaceSource?.source;
364
+ if (normalizeGitSource(source) !== normalizeGitSource(target.marketplaceRepo)) {
365
+ throw new Error(`${target.host} marketplace ${target.marketplace} does not point to ${target.marketplaceRepo}`);
366
+ }
367
+ const root = target.host === 'claude' ? found.installLocation : found.root;
368
+ if (typeof root !== 'string' || root.length === 0) {
369
+ throw new Error(`${target.host} marketplace ${target.marketplace} has no observable checkout root`);
370
+ }
371
+ const head = await run('git', ['-C', root, 'rev-parse', 'HEAD'], { env, timeout: 30_000 });
372
+ return { installed: true, exact: head.stdout.trim() === target.marketplaceCommit, root, found };
373
+ }
374
+
375
+ async function observeStructuredTarget(target, command, env, run) {
376
+ const listed = await run(command, getPlatform(target.host).cli.list(), { env });
377
+ const plugin = exactPluginObservation(target, listed.stdout);
378
+ const marketplace = await observeMarketplace(target, command, env, run);
379
+ if (plugin.installed && !marketplace.installed) {
380
+ throw new Error(`${target.host} reports the plugin but not its frozen marketplace`);
381
+ }
382
+ return { plugin, marketplace, exact: plugin.exact === true && marketplace.exact === true };
383
+ }
384
+
385
+ async function bindStructuredMarketplace(target, command, env, run, observed) {
386
+ if (observed.marketplace.installed && observed.marketplace.exact) return false;
387
+ if (observed.marketplace.installed) {
388
+ await run(command, [
389
+ 'plugin', 'marketplace', 'remove', target.marketplace,
390
+ ...(target.host === 'codex' ? ['--json'] : []),
391
+ ], { env });
392
+ }
393
+ const platform = getPlatform(target.host);
394
+ const frozenRef = target.host === 'codex' ? target.marketplaceCommit : target.marketplaceRef;
395
+ await run(command, platform.cli.marketplaceAdd(target.marketplaceRepo, frozenRef), { env });
396
+ const rebound = await observeMarketplace(target, command, env, run);
397
+ if (!rebound.installed || !rebound.exact) {
398
+ throw new Error(`${target.host} marketplace did not bind to frozen commit ${target.marketplaceCommit}`);
399
+ }
400
+ return true;
401
+ }
402
+
403
+ function actionParametersForTarget(plan, target) {
404
+ const action = (plan.externalActions ?? []).find((candidate) => candidate.id === target.actionId);
405
+ if (!action || action.type !== target.actionType || action.unitId !== target.unitId) {
406
+ throw new Error(`local host update target ${target.actionId} has no matching frozen action`);
407
+ }
408
+ return action.parameters;
409
+ }
410
+
411
+ async function verifyStructuredInstalledPayload({
412
+ plan,
413
+ root,
414
+ target,
415
+ installPath,
416
+ verifyInstalledPayload,
417
+ }) {
418
+ if (typeof installPath !== 'string' || installPath.length === 0) {
419
+ throw new Error(`${target.host} did not provide an authoritative installed plugin root`);
420
+ }
421
+ await verifyInstalledPayload(
422
+ actionParametersForTarget(plan, target),
423
+ { root },
424
+ installPath,
425
+ target.host,
426
+ );
427
+ }
428
+
429
+ async function runStructuredUpdate(target, detected, run, {
430
+ plan,
431
+ root,
432
+ verifyInstalledPayload,
433
+ }) {
434
+ if (target.host === 'codebuddy' || target.host === 'workbuddy') {
435
+ const env = hostEnvironment(target.host);
436
+ const listed = await run(detected.command, ['plugin', 'list', '--json'], { env });
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
+ }
448
+ if (observed.exact) return { status: 'ALREADY_CURRENT', version: target.version };
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 };
482
+ }
483
+
484
+ const command = detected.command;
485
+ const env = hostEnvironment(target.host);
486
+ const before = await observeStructuredTarget(target, command, env, run);
487
+ if (before.exact && before.plugin.installPath) {
488
+ await verifyStructuredInstalledPayload({
489
+ plan,
490
+ root,
491
+ target,
492
+ installPath: before.plugin.installPath,
493
+ verifyInstalledPayload,
494
+ });
495
+ return { status: 'ALREADY_CURRENT', version: target.version };
496
+ }
497
+
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;
502
+ const platform = getPlatform(target.host);
503
+ const selector = `${target.plugin}@${target.marketplace}`;
504
+ let installPath;
505
+ if (target.host === 'claude') {
506
+ const installArgs = current.plugin.installed
507
+ ? ['plugin', 'update', selector, '--scope', 'user', '--yes']
508
+ : platform.cli.install(target.plugin, target.marketplace);
509
+ await run(command, installArgs, { env });
510
+ } else {
511
+ if (before.plugin.installed) {
512
+ await run(command, ['plugin', 'remove', selector, '--json'], { env });
513
+ }
514
+ const installed = await run(command, platform.cli.install(target.plugin, target.marketplace), { env });
515
+ const installOutput = parseJson(installed.stdout, `${target.host} plugin install`);
516
+ const extracted = platform.strategy.extractInstallPath({ execEvidence: { installOutput } });
517
+ if (!extracted.ok) throw new Error(extracted.error);
518
+ installPath = extracted.installPath;
519
+ }
520
+
521
+ const after = await observeStructuredTarget(target, command, env, run);
522
+ if (!after.exact) throw new Error(`${target.host} did not install the frozen plugin identity`);
523
+ await verifyStructuredInstalledPayload({
524
+ plan,
525
+ root,
526
+ target,
527
+ installPath: installPath ?? after.plugin.installPath,
528
+ verifyInstalledPayload,
529
+ });
530
+ return { status: 'UPDATED', version: target.version, restartRequired: true };
531
+ }
532
+
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
+ `;
604
+ }
605
+
606
+ async function observeKimiTarget(target, kimiHome, run) {
607
+ const pluginsRoot = join(kimiHome, 'plugins');
608
+ let installed;
609
+ try {
610
+ installed = parseJson(
611
+ await readFileContained(pluginsRoot, 'installed.json', { encoding: 'utf8' }),
612
+ 'Kimi installed plugin registry',
613
+ );
614
+ } catch (error) {
615
+ if (
616
+ error?.code === 'ENOENT'
617
+ || error?.details?.causeCode === 'ENOENT'
618
+ || error?.details?.kind === 'missing-resource'
619
+ ) return { installed: false };
620
+ throw error;
621
+ }
622
+ const entry = installed?.plugins?.find((candidate) => candidate?.id === target.plugin);
623
+ if (!entry) return { installed: false };
624
+ if (!entry.github) return { installed: true, exact: false, source: 'legacy', entry };
625
+ const managedRoot = join(pluginsRoot, 'managed');
626
+ const pluginRoot = await resolveContained(managedRoot, target.plugin);
627
+ const declaredRoot = await resolveContained(managedRoot, relative(managedRoot, entry.root));
628
+ if (declaredRoot !== pluginRoot) throw new Error('Kimi installed plugin root does not match its managed root');
629
+ const packageJson = parseJson(
630
+ await readFileContained(pluginRoot, 'package.json', { encoding: 'utf8' }),
631
+ 'Kimi installed package manifest',
632
+ );
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;
645
+ }
646
+ const exact = packageJson.name === target.plugin
647
+ && packageJson.version === target.version
648
+ && refName === target.pluginTag
649
+ && revision === target.pluginCommit
650
+ && gitHeadExact;
651
+ return {
652
+ installed: true,
653
+ exact,
654
+ source: 'github',
655
+ entry,
656
+ packageJson,
657
+ refName,
658
+ revision,
659
+ pluginRoot,
660
+ };
661
+ }
662
+
663
+ async function runKimiUpdate(target, detected, run, kimiHome, {
664
+ plan,
665
+ root,
666
+ verifyInstalledPayload,
667
+ }) {
668
+ const env = hostEnvironment('kimi', { kimiHome });
669
+ const before = await observeKimiTarget(target, kimiHome, run);
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
+ }
680
+ await withTemporaryWorkspace(async (workspace) => {
681
+ const checkout = join(workspace.root, 'plugin');
682
+ await run('git', [
683
+ 'clone', '--depth', '1', '--branch', target.pluginTag, '--single-branch',
684
+ `https://github.com/${target.pluginRepo}.git`, checkout,
685
+ ], { timeout: 180_000, env });
686
+ const observed = await run('git', ['-C', checkout, 'rev-parse', 'HEAD'], {
687
+ env,
688
+ timeout: 30_000,
689
+ });
690
+ if (observed.stdout.trim() !== target.pluginCommit) {
691
+ throw new Error(`Kimi checkout commit does not match frozen release commit ${target.pluginCommit}`);
692
+ }
693
+ const installUrl = `https://github.com/${target.pluginRepo}/releases/tag/${target.pluginTag}`;
694
+ const removePlugin = before.source === 'legacy' ? target.plugin : '';
695
+ await run(detected.expectCommand, ['-c', kimiExpectProgram({
696
+ ...(removePlugin ? { removePlugin } : {}),
697
+ })], {
698
+ timeout: 240_000,
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
+ },
705
+ });
706
+ }, { prefix: 'release-skill-kimi-update-' });
707
+ const after = await observeKimiTarget(target, kimiHome, run);
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
+ });
716
+ return { status: 'UPDATED', version: target.version, restartRequired: true };
717
+ }
718
+
719
+ function aggregateStatus(results) {
720
+ const statuses = new Set(results.map((entry) => entry.status));
721
+ const completed = results.some((entry) => ['UPDATED', 'ALREADY_CURRENT'].includes(entry.status));
722
+ const incomplete = results.some((entry) => ['FAILED', 'MANUAL_REQUIRED'].includes(entry.status));
723
+ if (completed && incomplete) return 'PARTIAL';
724
+ if (statuses.has('FAILED')) return 'FAILED';
725
+ if (statuses.has('MANUAL_REQUIRED')) return 'MANUAL_REQUIRED';
726
+ if (statuses.has('UPDATED')) return 'UPDATED';
727
+ if (statuses.has('ALREADY_CURRENT')) return 'ALREADY_CURRENT';
728
+ return 'NO_APPLICABLE_HOSTS';
729
+ }
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
+
744
+ export async function updateLocalHostPlugins({
745
+ plan,
746
+ root = process.cwd(),
747
+ confirmPlanDigest,
748
+ selectedHosts,
749
+ detect = (host) => defaultDetect(host, defaultRun),
750
+ run = defaultRun,
751
+ kimiHome,
752
+ verifyInstalledPayload = verifyInstalledMarketplacePayload,
753
+ } = {}) {
754
+ const effectiveKimiHome = kimiHome ?? process.env.KIMI_CONFIG_DIR ?? join(homedir(), '.kimi-code');
755
+ const checklist = derivePostReleaseChecklist(plan);
756
+ if (confirmPlanDigest !== plan.digest) {
757
+ throw new Error('plan digest confirmation does not match the frozen release plan');
758
+ }
759
+ const selected = new Set(Array.isArray(selectedHosts) ? selectedHosts : []);
760
+ const unknown = [...selected].filter((host) => !checklist.localHostUpdate.hosts.includes(host));
761
+ if (unknown.length > 0) throw new Error(`selected hosts are not declared by the plan: ${unknown.join(', ')}`);
762
+ const targets = checklist.localHostUpdate.targets.filter((item) => selected.has(item.host));
763
+ for (const target of targets) assertExecutableTarget(target);
764
+
765
+ const results = [];
766
+ for (const target of targets) {
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
+ }
778
+ const outcome = target.host === 'kimi'
779
+ ? await runKimiUpdate(target, detected, run, effectiveKimiHome, {
780
+ plan,
781
+ root,
782
+ verifyInstalledPayload,
783
+ })
784
+ : await runStructuredUpdate(target, detected, run, {
785
+ plan,
786
+ root,
787
+ verifyInstalledPayload,
788
+ });
789
+ results.push({ host: target.host, unitId: target.unitId, ...outcome });
790
+ } catch (error) {
791
+ results.push(failedHostResult(target, error));
792
+ }
793
+ }
794
+ return {
795
+ command: 'post-release',
796
+ operation: 'update-local-hosts',
797
+ status: aggregateStatus(results),
798
+ planDigest: plan.digest,
799
+ results,
800
+ releaseStatusChanged: false,
801
+ };
802
+ }