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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +58 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +52 -16
- package/README.zh-CN.md +46 -16
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/claude/schemas/release-plan.schema.json +36 -0
- package/adapters/claude/schemas/release-project.schema.json +31 -0
- package/adapters/claude/skills/release-finish/SKILL.md +5 -5
- package/adapters/claude/skills/release-help/SKILL.md +11 -4
- package/adapters/claude/skills/release-prepare/SKILL.md +11 -3
- package/adapters/claude/skills/release-verify/SKILL.md +2 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/codex/schemas/release-plan.schema.json +36 -0
- package/adapters/codex/schemas/release-project.schema.json +31 -0
- package/adapters/codex/skills/release-finish/SKILL.md +5 -5
- package/adapters/codex/skills/release-help/SKILL.md +11 -4
- package/adapters/codex/skills/release-prepare/SKILL.md +11 -3
- package/adapters/codex/skills/release-verify/SKILL.md +2 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/kimi/schemas/release-plan.schema.json +36 -0
- package/adapters/kimi/schemas/release-project.schema.json +31 -0
- package/adapters/kimi/skills/release-finish/SKILL.md +5 -5
- package/adapters/kimi/skills/release-help/SKILL.md +11 -4
- package/adapters/kimi/skills/release-prepare/SKILL.md +11 -3
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/workbuddy/schemas/release-plan.schema.json +36 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +31 -0
- package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -5
- package/adapters/workbuddy/skills/release-help/SKILL.md +11 -4
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +11 -3
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -1
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/release-skill-cli.mjs +113 -22
- package/bin/release-skill.bundle.mjs +1513 -352
- package/package.json +4 -4
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +4 -4
- package/references/01-state-machine.md +18 -0
- package/references/02-project-config.md +20 -0
- package/schemas/release-plan.schema.json +36 -0
- package/schemas/release-project.schema.json +31 -0
- package/skills/release-finish/SKILL.md +5 -5
- package/skills/release-help/SKILL.md +11 -4
- package/skills/release-prepare/SKILL.md +11 -3
- package/skills/release-verify/SKILL.md +2 -1
- package/skills-src/release-finish/SKILL.md +5 -5
- package/skills-src/release-help/SKILL.md +11 -4
- package/skills-src/release-prepare/SKILL.md +11 -3
- package/skills-src/release-verify/SKILL.md +2 -1
- package/src/commands/post-release-local.mjs +373 -26
- package/src/commands/prepare.mjs +361 -16
- package/src/commands/ship.mjs +190 -13
- package/src/commands/verify.mjs +175 -2
- package/src/core/foundation-plugin-verification.mjs +22 -2
- package/src/core/plan.mjs +45 -1
- package/src/platforms/registry.mjs +97 -0
- package/src/producers/foundation-resource-projection.mjs +3 -3
- 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
|
-
|
|
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 === '
|
|
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'
|
|
280
|
-
for (const command of ['codebuddy', 'cbc'
|
|
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
|
|
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)
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
'plugin', 'update',
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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 {
|
|
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
|
-
...(
|
|
1087
|
+
...(publicDetails !== undefined ? { details: publicDetails } : {}),
|
|
741
1088
|
};
|
|
742
1089
|
}
|
|
743
1090
|
|
|
744
|
-
|
|
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;
|