release-skill 0.9.18 → 0.9.19
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/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +1 -1
- package/CHANGELOG.md +34 -0
- package/INSTALL.md +12 -2
- package/INSTALL.zh-CN.md +10 -3
- package/README.md +27 -17
- package/README.zh-CN.md +19 -17
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +924 -378
- package/adapters/claude/skills/release-finish/SKILL.md +30 -4
- package/adapters/claude/skills/release-help/SKILL.md +44 -101
- package/adapters/claude/skills/release-verify/SKILL.md +2 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +924 -378
- package/adapters/codex/skills/release-finish/SKILL.md +30 -4
- package/adapters/codex/skills/release-help/SKILL.md +46 -103
- package/adapters/codex/skills/release-verify/SKILL.md +2 -2
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +924 -378
- package/adapters/cursor/skills/release-finish/SKILL.md +30 -4
- package/adapters/cursor/skills/release-help/SKILL.md +46 -103
- package/adapters/cursor/skills/release-verify/SKILL.md +2 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +924 -378
- package/adapters/kimi/skills/release-finish/SKILL.md +30 -4
- package/adapters/kimi/skills/release-help/SKILL.md +46 -103
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -2
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +924 -378
- package/adapters/qoder/skills/release-finish/SKILL.md +30 -4
- package/adapters/qoder/skills/release-help/SKILL.md +46 -103
- package/adapters/qoder/skills/release-verify/SKILL.md +2 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +924 -378
- package/adapters/workbuddy/skills/release-finish/SKILL.md +30 -4
- package/adapters/workbuddy/skills/release-help/SKILL.md +44 -101
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -2
- package/bin/release-skill-cli.mjs +90 -14
- package/bin/release-skill.bundle.mjs +924 -378
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/02-project-config.md +4 -0
- package/skills/release-finish/SKILL.md +30 -4
- package/skills/release-help/SKILL.md +44 -101
- package/skills/release-verify/SKILL.md +2 -2
- package/skills-src/release-finish/SKILL.md +30 -4
- package/skills-src/release-help/SKILL.md +44 -101
- package/skills-src/release-verify/SKILL.md +2 -2
- package/src/commands/post-release-finish.mjs +448 -0
- package/src/commands/post-release-local.mjs +22 -2
- package/src/commands/ship.mjs +1 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, normalize, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { readFileStrict } from 'skill-family-harness-node';
|
|
5
|
+
|
|
6
|
+
import { loadProjectConfig } from '../core/config.mjs';
|
|
7
|
+
import {
|
|
8
|
+
derivePostReleaseChecklist,
|
|
9
|
+
runLocalFinishCommand,
|
|
10
|
+
updateLocalHostPlugins,
|
|
11
|
+
} from './post-release-local.mjs';
|
|
12
|
+
|
|
13
|
+
const STEP_STATUSES = new Set(['COMPLETE', 'PENDING', 'FAILED', 'SKIPPED']);
|
|
14
|
+
const FEEDBACK_FIELDS = new Set(['planDigest', 'configDigest', 'projectRoot', 'merge', 'hosts', 'setup']);
|
|
15
|
+
const MERGE_FIELDS = new Set(['outcome', 'summary']);
|
|
16
|
+
const HOST_FIELDS = new Set(['unitId', 'host', 'installation', 'loaded', 'plugin', 'version', 'skillFile', 'summary']);
|
|
17
|
+
const SETUP_FIELDS = new Set(['host', 'unitId', 'skillFile', 'outcome', 'summary']);
|
|
18
|
+
|
|
19
|
+
function fail(message) {
|
|
20
|
+
const error = new Error(message);
|
|
21
|
+
error.code = 'POST_RELEASE_FINISH_INVALID';
|
|
22
|
+
error.exitCode = 1;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function assertPlainObject(value, label) {
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${label} must be an object`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertClosed(value, allowed, label) {
|
|
31
|
+
assertPlainObject(value, label);
|
|
32
|
+
const unknown = Object.keys(value).filter((field) => !allowed.has(field));
|
|
33
|
+
if (unknown.length > 0) fail(`${label} contains unknown fields: ${unknown.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nonEmptyString(value, label) {
|
|
37
|
+
if (typeof value !== 'string' || value.trim().length === 0) fail(`${label} must be a non-empty string`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizedAbsolutePath(value, label) {
|
|
42
|
+
nonEmptyString(value, label);
|
|
43
|
+
if (!isAbsolute(value) || normalize(value) !== value) fail(`${label} must be a normalized absolute path`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readFeedback(feedbackPath) {
|
|
48
|
+
if (feedbackPath === undefined) return null;
|
|
49
|
+
normalizedAbsolutePath(feedbackPath, 'finish feedback path');
|
|
50
|
+
let receipt;
|
|
51
|
+
try {
|
|
52
|
+
receipt = await readFileStrict(dirname(feedbackPath), basename(feedbackPath), { encoding: 'utf8' });
|
|
53
|
+
} catch (cause) {
|
|
54
|
+
fail(`cannot strictly read finish feedback: ${cause.message}`);
|
|
55
|
+
}
|
|
56
|
+
let value;
|
|
57
|
+
try {
|
|
58
|
+
value = JSON.parse(receipt.content);
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
fail(`finish feedback is not valid JSON: ${cause.message}`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function targetKey(unitId, host) {
|
|
66
|
+
return `${unitId}\u0000${host}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateFeedback(raw, { projectRoot, planDigest, configDigest, selectedTargets, setupSkill }) {
|
|
70
|
+
if (raw === null) return { merge: null, hosts: [], setup: null };
|
|
71
|
+
assertClosed(raw, FEEDBACK_FIELDS, 'finish feedback');
|
|
72
|
+
for (const field of ['planDigest', 'configDigest', 'projectRoot']) nonEmptyString(raw[field], `finish feedback.${field}`);
|
|
73
|
+
if (raw.planDigest !== planDigest) fail('finish feedback planDigest does not match the frozen plan');
|
|
74
|
+
if (raw.configDigest !== configDigest) fail('finish feedback configDigest does not match the current project configuration');
|
|
75
|
+
if (normalizedAbsolutePath(raw.projectRoot, 'finish feedback.projectRoot') !== projectRoot) {
|
|
76
|
+
fail('finish feedback projectRoot does not match the current project root');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let merge = null;
|
|
80
|
+
if (raw.merge !== undefined) {
|
|
81
|
+
assertClosed(raw.merge, MERGE_FIELDS, 'finish feedback.merge');
|
|
82
|
+
if (!['completed', 'skipped', 'pending'].includes(raw.merge.outcome)) fail('finish feedback.merge.outcome is invalid');
|
|
83
|
+
nonEmptyString(raw.merge.summary, 'finish feedback.merge.summary');
|
|
84
|
+
merge = { ...raw.merge, basis: 'agent-reported' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const targetsByKey = new Map(selectedTargets.map((target) => [targetKey(target.unitId, target.host), target]));
|
|
88
|
+
const hosts = [];
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
if (raw.hosts !== undefined && !Array.isArray(raw.hosts)) fail('finish feedback.hosts must be an array');
|
|
91
|
+
for (const [index, host] of (raw.hosts ?? []).entries()) {
|
|
92
|
+
assertClosed(host, HOST_FIELDS, `finish feedback.hosts[${index}]`);
|
|
93
|
+
for (const field of ['unitId', 'host', 'plugin', 'version', 'summary']) {
|
|
94
|
+
nonEmptyString(host[field], `finish feedback.hosts[${index}].${field}`);
|
|
95
|
+
}
|
|
96
|
+
if (!['current', 'pending', 'failed'].includes(host.installation)) fail(`finish feedback.hosts[${index}].installation is invalid`);
|
|
97
|
+
if (typeof host.loaded !== 'boolean') fail(`finish feedback.hosts[${index}].loaded must be boolean`);
|
|
98
|
+
if (host.installation !== 'current' && host.loaded) fail(`finish feedback.hosts[${index}] cannot be loaded unless installation is current`);
|
|
99
|
+
if (host.skillFile !== undefined) normalizedAbsolutePath(host.skillFile, `finish feedback.hosts[${index}].skillFile`);
|
|
100
|
+
const key = targetKey(host.unitId, host.host);
|
|
101
|
+
if (seen.has(key)) fail(`finish feedback contains duplicate host result for ${host.unitId}/${host.host}`);
|
|
102
|
+
seen.add(key);
|
|
103
|
+
const target = targetsByKey.get(key);
|
|
104
|
+
if (!target) fail(`finish feedback host ${host.unitId}/${host.host} is outside the selected frozen targets`);
|
|
105
|
+
if (host.plugin !== target.plugin || host.version !== target.version) {
|
|
106
|
+
fail(`finish feedback host ${host.unitId}/${host.host} does not match the frozen plugin identity`);
|
|
107
|
+
}
|
|
108
|
+
hosts.push({ ...host, basis: 'agent-reported' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let setup = null;
|
|
112
|
+
if (raw.setup !== undefined) {
|
|
113
|
+
if (!setupSkill) fail('finish feedback.setup is not allowed when releaseFinish.setupSkill is not configured');
|
|
114
|
+
assertClosed(raw.setup, SETUP_FIELDS, 'finish feedback.setup');
|
|
115
|
+
for (const field of ['host', 'unitId', 'skillFile', 'summary']) nonEmptyString(raw.setup[field], `finish feedback.setup.${field}`);
|
|
116
|
+
normalizedAbsolutePath(raw.setup.skillFile, 'finish feedback.setup.skillFile');
|
|
117
|
+
if (!['completed', 'pending', 'failed'].includes(raw.setup.outcome)) fail('finish feedback.setup.outcome is invalid');
|
|
118
|
+
const host = hosts.find((entry) => entry.host === raw.setup.host && entry.unitId === raw.setup.unitId);
|
|
119
|
+
if (!host || host.installation !== 'current' || host.loaded !== true || host.skillFile !== raw.setup.skillFile) {
|
|
120
|
+
fail('finish feedback.setup is not bound to one selected, current, loaded host and matching skillFile');
|
|
121
|
+
}
|
|
122
|
+
setup = { ...raw.setup, basis: 'agent-reported' };
|
|
123
|
+
}
|
|
124
|
+
return { merge, hosts, setup };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function step(status, summary, extra = {}) {
|
|
128
|
+
if (!STEP_STATUSES.has(status)) throw new Error(`unknown finish step status: ${status}`);
|
|
129
|
+
return { status, summary, ...extra };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resultToInstallation(result, target) {
|
|
133
|
+
if (!result) return null;
|
|
134
|
+
if (['UPDATED', 'ALREADY_CURRENT'].includes(result.status) && result.version !== target.version) {
|
|
135
|
+
return {
|
|
136
|
+
unitId: target.unitId, host: target.host, plugin: target.plugin, version: target.version,
|
|
137
|
+
installation: 'failed', loaded: false,
|
|
138
|
+
summary: `宿主更新结果版本 ${result.version ?? '(missing)'} 与冻结版本 ${target.version} 不一致。`,
|
|
139
|
+
basis: 'script-observed', updateStatus: result.status,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (['UPDATED', 'ALREADY_CURRENT'].includes(result.status)) {
|
|
143
|
+
return {
|
|
144
|
+
unitId: target.unitId,
|
|
145
|
+
host: target.host,
|
|
146
|
+
plugin: target.plugin,
|
|
147
|
+
version: target.version,
|
|
148
|
+
installation: 'current',
|
|
149
|
+
loaded: false,
|
|
150
|
+
summary: result.status === 'UPDATED'
|
|
151
|
+
? '脚本已更新并复验安装载荷;仍需由当前智能体确认宿主实际加载。'
|
|
152
|
+
: '脚本已确认安装载荷与冻结版本一致;仍需由当前智能体确认宿主实际加载。',
|
|
153
|
+
basis: 'script-observed',
|
|
154
|
+
updateStatus: result.status,
|
|
155
|
+
...(result.restartRequired ? { restartRequired: true } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
unitId: target.unitId,
|
|
160
|
+
host: target.host,
|
|
161
|
+
plugin: target.plugin,
|
|
162
|
+
version: target.version,
|
|
163
|
+
installation: result.status === 'FAILED' ? 'failed' : 'pending',
|
|
164
|
+
loaded: false,
|
|
165
|
+
summary: result.reason ?? result.error ?? `宿主更新结果为 ${result.status}。`,
|
|
166
|
+
basis: 'script-observed',
|
|
167
|
+
updateStatus: result.status,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function gitReadEnvironment() {
|
|
172
|
+
const env = { GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0' };
|
|
173
|
+
for (const key of ['PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL']) {
|
|
174
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
175
|
+
}
|
|
176
|
+
return env;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function observeGitCommand({ root, args, run }) {
|
|
180
|
+
const argv = ['git', ...args];
|
|
181
|
+
try {
|
|
182
|
+
const result = await run('git', args, { cwd: root, env: gitReadEnvironment() });
|
|
183
|
+
return { argv, status: 'SUCCEEDED', exitStatus: 0, stdout: result.stdout, stderr: result.stderr };
|
|
184
|
+
} catch (cause) {
|
|
185
|
+
return {
|
|
186
|
+
argv,
|
|
187
|
+
status: 'FAILED',
|
|
188
|
+
exitStatus: cause?.exitStatus ?? cause?.code ?? null,
|
|
189
|
+
stdout: cause?.stdout ?? cause?.foundationStdout ?? '',
|
|
190
|
+
stderr: cause?.stderr ?? cause?.foundationStderr ?? '',
|
|
191
|
+
message: cause?.message ?? String(cause),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function inspectSourceBranch({ root, config, run }) {
|
|
197
|
+
const policy = config.releaseFinish?.sourceBranchCheck ?? 'remind';
|
|
198
|
+
const targetBranch = config.project.defaultBranch;
|
|
199
|
+
if (policy === 'skip') {
|
|
200
|
+
return step('SKIPPED', '项目配置已关闭源码分支检查。', { policy, targetBranch, basis: 'script-observed' });
|
|
201
|
+
}
|
|
202
|
+
const branchResult = await observeGitCommand({ root, args: ['branch', '--show-current'], run });
|
|
203
|
+
const statusResult = await observeGitCommand({ root, args: ['status', '--short', '--branch'], run });
|
|
204
|
+
const commands = [branchResult, statusResult];
|
|
205
|
+
if (commands.some((command) => command.status === 'FAILED')) {
|
|
206
|
+
return step('FAILED', '源码分支只读检查至少一条 Git 命令失败。', {
|
|
207
|
+
policy, targetBranch, commands, basis: 'script-observed',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const currentBranch = branchResult.stdout.trim();
|
|
212
|
+
const lines = statusResult.stdout.replace(/\r/gu, '').split('\n').filter(Boolean);
|
|
213
|
+
const tracking = lines[0]?.startsWith('## ') ? lines[0].slice(3) : '';
|
|
214
|
+
const changed = (lines[0]?.startsWith('## ') ? lines.slice(1) : lines).length > 0;
|
|
215
|
+
const detached = currentBranch.length === 0;
|
|
216
|
+
const aligned = !detached && currentBranch === targetBranch;
|
|
217
|
+
return step('COMPLETE', detached
|
|
218
|
+
? '已完成只读检查;当前为 detached HEAD,未猜测后续 Git 操作。'
|
|
219
|
+
: aligned
|
|
220
|
+
? `已完成只读检查;当前分支为目标分支 ${targetBranch}。`
|
|
221
|
+
: `已完成只读检查;当前分支 ${currentBranch} 与目标分支 ${targetBranch} 不同。`, {
|
|
222
|
+
policy,
|
|
223
|
+
targetBranch,
|
|
224
|
+
currentBranch: detached ? null : currentBranch,
|
|
225
|
+
detached,
|
|
226
|
+
aligned,
|
|
227
|
+
changed,
|
|
228
|
+
tracking: tracking || null,
|
|
229
|
+
rawStatus: statusResult.stdout,
|
|
230
|
+
commands,
|
|
231
|
+
basis: 'script-observed',
|
|
232
|
+
});
|
|
233
|
+
} catch (cause) {
|
|
234
|
+
return step('FAILED', `源码分支只读检查失败:${cause.message}`, {
|
|
235
|
+
policy, targetBranch, commands, basis: 'script-observed',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function finishStatus(steps) {
|
|
241
|
+
const statuses = Object.values(steps).map((entry) => entry.status);
|
|
242
|
+
if (statuses.includes('FAILED')) return 'FAILED';
|
|
243
|
+
if (statuses.includes('PENDING')) return 'PENDING';
|
|
244
|
+
return 'COMPLETE';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function runPostReleaseFinish({
|
|
248
|
+
root = process.cwd(),
|
|
249
|
+
plan,
|
|
250
|
+
planPath,
|
|
251
|
+
runPath,
|
|
252
|
+
selectedHosts = [],
|
|
253
|
+
updateRequested = false,
|
|
254
|
+
skipLocalHosts = false,
|
|
255
|
+
confirmPlanDigest,
|
|
256
|
+
cursorPluginsRoot,
|
|
257
|
+
feedbackPath,
|
|
258
|
+
updateLocalHostPluginsFn = updateLocalHostPlugins,
|
|
259
|
+
run = runLocalFinishCommand,
|
|
260
|
+
} = {}) {
|
|
261
|
+
const projectRoot = await realpath(resolve(root));
|
|
262
|
+
const loaded = await loadProjectConfig({ root: projectRoot });
|
|
263
|
+
const checklist = derivePostReleaseChecklist(plan, {
|
|
264
|
+
root: projectRoot,
|
|
265
|
+
planPath,
|
|
266
|
+
runPath,
|
|
267
|
+
postVerifyComplete: true,
|
|
268
|
+
});
|
|
269
|
+
const selected = new Set(selectedHosts);
|
|
270
|
+
const unknownHosts = [...selected].filter((host) => !checklist.localHostUpdate.hosts.includes(host));
|
|
271
|
+
if (unknownHosts.length > 0) fail(`selected hosts are not declared by the plan: ${unknownHosts.join(', ')}`);
|
|
272
|
+
const selectedTargets = checklist.localHostUpdate.targets.filter((target) => selected.has(target.host));
|
|
273
|
+
const feedback = validateFeedback(await readFeedback(feedbackPath), {
|
|
274
|
+
projectRoot,
|
|
275
|
+
planDigest: plan.digest,
|
|
276
|
+
configDigest: loaded.configDigest,
|
|
277
|
+
selectedTargets,
|
|
278
|
+
setupSkill: loaded.config.releaseFinish?.setupSkill,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let update = null;
|
|
282
|
+
if (updateRequested) {
|
|
283
|
+
update = await updateLocalHostPluginsFn({
|
|
284
|
+
planPath,
|
|
285
|
+
runPath,
|
|
286
|
+
root: projectRoot,
|
|
287
|
+
confirmPlanDigest,
|
|
288
|
+
selectedHosts,
|
|
289
|
+
cursorPluginsRoot,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const nextActions = [];
|
|
294
|
+
let mergeStep;
|
|
295
|
+
if (!checklist.merge.promptRequired) {
|
|
296
|
+
mergeStep = step('COMPLETE', '发布流程已覆盖分支推进,无需额外合并决定。', { basis: 'script-observed' });
|
|
297
|
+
} else if (!feedback.merge) {
|
|
298
|
+
mergeStep = step('PENDING', '尚未提供剩余发布分支的处理结果。');
|
|
299
|
+
nextActions.push({ type: 'decide-merge', units: checklist.merge.units });
|
|
300
|
+
} else {
|
|
301
|
+
const status = feedback.merge.outcome === 'completed' ? 'COMPLETE'
|
|
302
|
+
: feedback.merge.outcome === 'skipped' ? 'SKIPPED' : 'PENDING';
|
|
303
|
+
mergeStep = step(status, feedback.merge.summary, { basis: feedback.merge.basis, outcome: feedback.merge.outcome });
|
|
304
|
+
if (status === 'PENDING') nextActions.push({ type: 'decide-merge', units: checklist.merge.units });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const observations = [];
|
|
308
|
+
const feedbackByKey = new Map(feedback.hosts.map((entry) => [targetKey(entry.unitId, entry.host), entry]));
|
|
309
|
+
const updateByKey = new Map((update?.results ?? []).map((entry) => [targetKey(entry.unitId, entry.host), entry]));
|
|
310
|
+
const updateKeepsFeedback = !updateRequested || selectedTargets.every((target) => {
|
|
311
|
+
const result = updateByKey.get(targetKey(target.unitId, target.host));
|
|
312
|
+
return result?.status === 'ALREADY_CURRENT' && result.version === target.version;
|
|
313
|
+
});
|
|
314
|
+
for (const target of selectedTargets) {
|
|
315
|
+
const reported = feedbackByKey.get(targetKey(target.unitId, target.host));
|
|
316
|
+
const updateResult = updateByKey.get(targetKey(target.unitId, target.host));
|
|
317
|
+
const scriptObservation = resultToInstallation(updateResult, target);
|
|
318
|
+
if (!scriptObservation) {
|
|
319
|
+
if (reported) observations.push(reported);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (scriptObservation.installation === 'current'
|
|
323
|
+
&& scriptObservation.updateStatus === 'ALREADY_CURRENT'
|
|
324
|
+
&& reported?.installation === 'current') {
|
|
325
|
+
observations.push({
|
|
326
|
+
...scriptObservation,
|
|
327
|
+
loaded: reported.loaded,
|
|
328
|
+
...(reported.skillFile ? { skillFile: reported.skillFile } : {}),
|
|
329
|
+
loadSummary: reported.summary,
|
|
330
|
+
loadBasis: 'agent-reported',
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
observations.push(scriptObservation);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let hostUpdateStep;
|
|
338
|
+
let hostLoadStep;
|
|
339
|
+
const hasTargets = checklist.localHostUpdate.targets.length > 0;
|
|
340
|
+
if (skipLocalHosts) {
|
|
341
|
+
hostUpdateStep = step('SKIPPED', '调用者已明确跳过本轮宿主更新。');
|
|
342
|
+
hostLoadStep = step('SKIPPED', '本轮跳过宿主更新,因此不检查目标插件加载。');
|
|
343
|
+
} else if (!hasTargets) {
|
|
344
|
+
hostUpdateStep = step('SKIPPED', '冻结计划没有适用的本机宿主目标。');
|
|
345
|
+
hostLoadStep = step('SKIPPED', '没有适用的宿主加载目标。');
|
|
346
|
+
} else if (selectedTargets.length === 0) {
|
|
347
|
+
hostUpdateStep = step('PENDING', '尚未选择本轮要处理的宿主。', { availableHosts: checklist.localHostUpdate.hosts });
|
|
348
|
+
hostLoadStep = step('PENDING', '需先选择宿主并确认安装结果。');
|
|
349
|
+
nextActions.push({ type: 'choose-local-hosts', hosts: checklist.localHostUpdate.hosts, targets: checklist.localHostUpdate.targets });
|
|
350
|
+
} else {
|
|
351
|
+
const failed = observations.filter((entry) => entry.installation === 'failed');
|
|
352
|
+
const current = observations.filter((entry) => entry.installation === 'current');
|
|
353
|
+
const missing = selectedTargets.filter((target) => !observations.some((entry) => entry.unitId === target.unitId && entry.host === target.host));
|
|
354
|
+
const pending = observations.filter((entry) => entry.installation === 'pending');
|
|
355
|
+
hostUpdateStep = step(failed.length > 0 ? 'FAILED' : (missing.length > 0 || pending.length > 0 ? 'PENDING' : 'COMPLETE'),
|
|
356
|
+
failed.length > 0 ? '至少一个所选宿主的安装或更新失败。'
|
|
357
|
+
: missing.length > 0 || pending.length > 0 ? '部分所选宿主缺少安装结果或仍待处理。'
|
|
358
|
+
: '所选宿主的安装均已确认为目标版本。',
|
|
359
|
+
{ observations });
|
|
360
|
+
const unloaded = observations.filter((entry) => entry.installation === 'current' && entry.loaded !== true);
|
|
361
|
+
const loadedHosts = observations.filter((entry) => entry.installation === 'current' && entry.loaded === true);
|
|
362
|
+
hostLoadStep = step(failed.length > 0 ? 'FAILED' : (loadedHosts.length !== selectedTargets.length ? 'PENDING' : 'COMPLETE'),
|
|
363
|
+
failed.length > 0 ? '宿主安装失败,无法完成加载确认。'
|
|
364
|
+
: loadedHosts.length !== selectedTargets.length ? '尚未确认全部所选宿主已实际加载目标插件和入口。'
|
|
365
|
+
: '当前智能体已报告全部所选宿主加载目标入口。',
|
|
366
|
+
{ observations });
|
|
367
|
+
if (missing.length > 0 || pending.length > 0) nextActions.push({ type: 'observe-host-installation', targets: [...missing, ...pending] });
|
|
368
|
+
if (unloaded.length > 0) nextActions.push({ type: 'check-host-load', projectRoot, observations: unloaded });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const setupSkill = loaded.config.releaseFinish?.setupSkill;
|
|
372
|
+
const candidates = observations.filter((entry) => (
|
|
373
|
+
entry.installation === 'current' && entry.loaded === true && typeof entry.skillFile === 'string'
|
|
374
|
+
));
|
|
375
|
+
const candidateOwners = new Set(candidates.map((entry) => `${entry.plugin}\u0000${entry.version}\u0000${entry.skillFile}`));
|
|
376
|
+
const effectiveSetup = updateKeepsFeedback ? feedback.setup : null;
|
|
377
|
+
let setupStep;
|
|
378
|
+
if (!setupSkill) {
|
|
379
|
+
setupStep = step('SKIPPED', '项目未配置 releaseFinish.setupSkill。');
|
|
380
|
+
} else if (skipLocalHosts) {
|
|
381
|
+
setupStep = step('SKIPPED', '调用者已明确跳过宿主更新和加载,本轮不执行 setup。', { setupSkill });
|
|
382
|
+
} else if (candidateOwners.size > 1) {
|
|
383
|
+
setupStep = step('PENDING', `目标入口 ${setupSkill} 存在多个插件身份归属,不能用单个 setup 反馈完整收尾。`, {
|
|
384
|
+
setupSkill, candidates,
|
|
385
|
+
});
|
|
386
|
+
nextActions.push({ type: 'disambiguate-setup', setupSkill, projectRoot, candidates });
|
|
387
|
+
} else if (effectiveSetup) {
|
|
388
|
+
const status = effectiveSetup.outcome === 'completed' ? 'COMPLETE'
|
|
389
|
+
: effectiveSetup.outcome === 'failed' ? 'FAILED' : 'PENDING';
|
|
390
|
+
setupStep = step(status, effectiveSetup.summary, {
|
|
391
|
+
setupSkill,
|
|
392
|
+
result: effectiveSetup,
|
|
393
|
+
basis: 'agent-reported',
|
|
394
|
+
});
|
|
395
|
+
if (status === 'PENDING') nextActions.push({ type: 'invoke-setup', setupSkill, projectRoot, resume: effectiveSetup });
|
|
396
|
+
} else {
|
|
397
|
+
setupStep = step('PENDING', candidates.length > 0
|
|
398
|
+
? `目标入口 ${setupSkill} 已有可用宿主,但未收到实际 setup 结果。`
|
|
399
|
+
: `目标入口 ${setupSkill} 尚缺可用的已加载宿主与技能元数据。`, { setupSkill, candidates });
|
|
400
|
+
if (candidates.length > 0) {
|
|
401
|
+
nextActions.push({
|
|
402
|
+
type: 'invoke-setup',
|
|
403
|
+
setupSkill,
|
|
404
|
+
projectRoot,
|
|
405
|
+
intent: 'read-only-diagnosis',
|
|
406
|
+
authorization: 'No new write authority is granted by this request.',
|
|
407
|
+
candidates: candidates.map((entry) => ({
|
|
408
|
+
unitId: entry.unitId,
|
|
409
|
+
host: entry.host,
|
|
410
|
+
plugin: entry.plugin,
|
|
411
|
+
version: entry.version,
|
|
412
|
+
skillFile: entry.skillFile,
|
|
413
|
+
})),
|
|
414
|
+
hostObservations: observations,
|
|
415
|
+
});
|
|
416
|
+
} else {
|
|
417
|
+
nextActions.push({
|
|
418
|
+
type: 'resolve-setup-target',
|
|
419
|
+
setupSkill,
|
|
420
|
+
projectRoot,
|
|
421
|
+
reason: hasTargets ? 'no-loaded-candidates' : 'no-host-targets',
|
|
422
|
+
targets: checklist.localHostUpdate.targets,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const sourceBranchStep = await inspectSourceBranch({ root: projectRoot, config: loaded.config, run });
|
|
428
|
+
const steps = {
|
|
429
|
+
merge: mergeStep,
|
|
430
|
+
'host-update': hostUpdateStep,
|
|
431
|
+
'host-load': hostLoadStep,
|
|
432
|
+
setup: setupStep,
|
|
433
|
+
'source-branch': sourceBranchStep,
|
|
434
|
+
};
|
|
435
|
+
return {
|
|
436
|
+
command: 'post-release',
|
|
437
|
+
...(update ? { operation: 'finish-with-local-host-update', localHostUpdate: update } : { checklist }),
|
|
438
|
+
finish: {
|
|
439
|
+
status: finishStatus(steps),
|
|
440
|
+
projectRoot,
|
|
441
|
+
planDigest: plan.digest,
|
|
442
|
+
configDigest: loaded.configDigest,
|
|
443
|
+
steps,
|
|
444
|
+
nextActions,
|
|
445
|
+
releaseStatusChanged: false,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
@@ -52,10 +52,11 @@ const QODER_PLUGIN_LIST_ARGS = Object.freeze(['plugins', 'list', '--json']);
|
|
|
52
52
|
const QODER_MARKETPLACE_LIST_ARGS = Object.freeze(['plugins', 'marketplace', 'list', '--json']);
|
|
53
53
|
const QODER_PAYLOAD_CONTRACT = 'external-marketplace-v1';
|
|
54
54
|
|
|
55
|
-
function attachFoundationFailure(error, { envelope, stdout }) {
|
|
55
|
+
function attachFoundationFailure(error, { envelope, stdout, stderr }) {
|
|
56
56
|
Object.defineProperties(error, {
|
|
57
57
|
foundationEnvelope: { value: envelope, enumerable: false },
|
|
58
58
|
foundationStdout: { value: stdout, enumerable: false },
|
|
59
|
+
foundationStderr: { value: stderr, enumerable: false },
|
|
59
60
|
});
|
|
60
61
|
return error;
|
|
61
62
|
}
|
|
@@ -220,6 +221,7 @@ function hubTargets(plan) {
|
|
|
220
221
|
unitId: declaration.unitId,
|
|
221
222
|
host,
|
|
222
223
|
plugin: local.plugin,
|
|
224
|
+
version: unit?.targetVersion,
|
|
223
225
|
hub,
|
|
224
226
|
message: `${manualInstruction[host]} Install or upgrade ${local.plugin} from Hub ${hub.name}; release-skill does not execute or probe this action.`,
|
|
225
227
|
...(unit?.publicRepo ? { publicRepo: unit.publicRepo } : {}),
|
|
@@ -314,9 +316,23 @@ function buildShipNextStep({ root, statePath, unitIds }) {
|
|
|
314
316
|
};
|
|
315
317
|
}
|
|
316
318
|
|
|
319
|
+
function buildFinishCommand({ root, planPath, runPath }) {
|
|
320
|
+
if (![root, planPath, runPath].every((value) => typeof value === 'string' && value.length > 0)) return undefined;
|
|
321
|
+
return {
|
|
322
|
+
argv: [
|
|
323
|
+
'release-skill', 'post-release',
|
|
324
|
+
'--root', root,
|
|
325
|
+
'--plan', planPath,
|
|
326
|
+
'--run', runPath,
|
|
327
|
+
'--finish',
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
317
332
|
export function derivePostReleaseChecklist(plan, {
|
|
318
333
|
runPath,
|
|
319
334
|
root,
|
|
335
|
+
planPath,
|
|
320
336
|
statePath,
|
|
321
337
|
unitIds,
|
|
322
338
|
postVerifyComplete = false,
|
|
@@ -338,10 +354,12 @@ export function derivePostReleaseChecklist(plan, {
|
|
|
338
354
|
const hasPendingPostVerify = postVerifyHooks(plan).length > 0 && !postVerifyComplete && targets.length > 0;
|
|
339
355
|
const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
|
|
340
356
|
const selectedUnitIds = Array.isArray(unitIds) ? unitIds : undefined;
|
|
357
|
+
const finishCommand = hasPendingPostVerify ? undefined : buildFinishCommand({ root, planPath, runPath });
|
|
341
358
|
return {
|
|
342
359
|
command: 'post-release',
|
|
343
360
|
status: 'AWAITING_USER_DECISION',
|
|
344
361
|
planDigest: plan.digest,
|
|
362
|
+
...(finishCommand ? { finishCommand } : {}),
|
|
345
363
|
merge: {
|
|
346
364
|
promptRequired: uncovered.length > 0,
|
|
347
365
|
alreadyHandledByRelease: uncovered.length === 0,
|
|
@@ -592,12 +610,14 @@ async function defaultRun(command, args, options = {}) {
|
|
|
592
610
|
watchdogReason: envelope.watchdogReason,
|
|
593
611
|
...(envelope.evidence?.spawnError ? { spawnError: envelope.evidence.spawnError } : {}),
|
|
594
612
|
};
|
|
595
|
-
throw attachFoundationFailure(error, { envelope, stdout });
|
|
613
|
+
throw attachFoundationFailure(error, { envelope, stdout, stderr });
|
|
596
614
|
}
|
|
597
615
|
return { stdout, stderr };
|
|
598
616
|
}, { prefix: 'release-skill-host-command-' });
|
|
599
617
|
}
|
|
600
618
|
|
|
619
|
+
export { defaultRun as runLocalFinishCommand };
|
|
620
|
+
|
|
601
621
|
async function commandAvailable(command, host, run) {
|
|
602
622
|
try {
|
|
603
623
|
await run(command, ['--version'], { timeout: 10_000, env: hostEnvironment(host) });
|
package/src/commands/ship.mjs
CHANGED
|
@@ -839,6 +839,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
839
839
|
try {
|
|
840
840
|
postRelease = derivePostReleaseChecklist(finalPlan, {
|
|
841
841
|
root,
|
|
842
|
+
planPath: state.planPath,
|
|
842
843
|
statePath,
|
|
843
844
|
unitIds: state.selectedUnitIds,
|
|
844
845
|
runPath: localFinishRunPath,
|