release-skill 0.2.9 → 0.4.0
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 +41 -0
- package/INSTALL.md +34 -110
- package/INSTALL.zh-CN.md +17 -83
- package/README.md +49 -41
- package/README.zh-CN.md +43 -34
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/claude/schemas/release-plan.schema.json +7 -0
- package/adapters/claude/schemas/release-run.schema.json +43 -0
- package/adapters/claude/skills/release-help/SKILL.md +10 -2
- package/adapters/claude/skills/release-prepare/SKILL.md +6 -15
- package/adapters/claude/skills/release-publish/SKILL.md +5 -6
- package/adapters/claude/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/claude/skills/release-verify/SKILL.md +4 -6
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/codex/schemas/release-plan.schema.json +7 -0
- package/adapters/codex/schemas/release-run.schema.json +43 -0
- package/adapters/codex/skills/release-help/SKILL.md +10 -2
- package/adapters/codex/skills/release-prepare/SKILL.md +6 -15
- package/adapters/codex/skills/release-publish/SKILL.md +5 -6
- package/adapters/codex/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/codex/skills/release-verify/SKILL.md +4 -6
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/kimi/schemas/release-plan.schema.json +7 -0
- package/adapters/kimi/schemas/release-run.schema.json +43 -0
- package/adapters/kimi/skills/release-help/SKILL.md +10 -2
- package/adapters/kimi/skills/release-prepare/SKILL.md +6 -15
- package/adapters/kimi/skills/release-publish/SKILL.md +5 -6
- package/adapters/kimi/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/kimi/skills/release-verify/SKILL.md +4 -6
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/workbuddy/schemas/release-plan.schema.json +7 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +43 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +10 -2
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +6 -15
- package/adapters/workbuddy/skills/release-publish/SKILL.md +5 -6
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -6
- package/bin/release-skill-cli.mjs +212 -43
- package/bin/release-skill.bundle.mjs +4131 -3180
- package/package.json +1 -1
- package/references/01-state-machine.md +1 -1
- package/references/06-adapter-contract.md +6 -5
- package/schemas/release-plan.schema.json +7 -0
- package/schemas/release-run.schema.json +43 -0
- package/skills/release-help/SKILL.md +10 -2
- package/skills/release-prepare/SKILL.md +6 -15
- package/skills/release-publish/SKILL.md +5 -6
- package/skills/release-reconcile/SKILL.md +2 -2
- package/skills/release-verify/SKILL.md +4 -6
- package/skills-src/release-help/SKILL.md +10 -2
- package/skills-src/release-prepare/SKILL.md +6 -15
- package/skills-src/release-publish/SKILL.md +5 -6
- package/skills-src/release-reconcile/SKILL.md +2 -2
- package/skills-src/release-verify/SKILL.md +4 -6
- package/src/adapters/plugin-marketplace.mjs +1 -1
- package/src/adapters/push-snapshot.mjs +3 -1
- package/src/commands/approve.mjs +8 -6
- package/src/commands/attest.mjs +195 -0
- package/src/commands/hooks.mjs +42 -0
- package/src/commands/prepare.mjs +13 -50
- package/src/commands/publish.mjs +0 -8
- package/src/commands/reconcile.mjs +3 -37
- package/src/commands/ship.mjs +334 -0
- package/src/commands/verify.mjs +195 -15
- package/src/core/git-transport.mjs +93 -0
- package/src/core/release-metadata.mjs +105 -0
- package/src/core/skill-resource-closure.mjs +7 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { readFile, lstat, mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
5
|
+
import {
|
|
6
|
+
ReleaseError,
|
|
7
|
+
GATE_FAILED,
|
|
8
|
+
PLAN_DIGEST_MISMATCH,
|
|
9
|
+
CONSUMER_VERIFICATION_DEFERRED,
|
|
10
|
+
} from '../core/errors.mjs';
|
|
11
|
+
|
|
12
|
+
async function writeJsonAtomic(path, value) {
|
|
13
|
+
await mkdir(dirname(path), { recursive: true });
|
|
14
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
15
|
+
const { stateDigest: _oldDigest, ...body } = value;
|
|
16
|
+
const sealed = {
|
|
17
|
+
...body,
|
|
18
|
+
stateDigest: sha256Hex(canonicalJson(body)),
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
await writeFile(temp, `${JSON.stringify(sealed, null, 2)}\n`, {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
mode: 0o600,
|
|
24
|
+
flag: 'wx',
|
|
25
|
+
});
|
|
26
|
+
await rename(temp, path);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
await rm(temp, { force: true }).catch(() => {});
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readState(path) {
|
|
34
|
+
try {
|
|
35
|
+
const stat = await lstat(path);
|
|
36
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
37
|
+
throw new Error('ship state must be a regular non-symlink file');
|
|
38
|
+
}
|
|
39
|
+
const state = JSON.parse(await readFile(path, 'utf8'));
|
|
40
|
+
const { stateDigest, ...body } = state;
|
|
41
|
+
if (
|
|
42
|
+
typeof stateDigest !== 'string'
|
|
43
|
+
|| stateDigest !== sha256Hex(canonicalJson(body))
|
|
44
|
+
|| body.statePath !== path
|
|
45
|
+
) {
|
|
46
|
+
throw new Error('ship state digest or authority path does not match');
|
|
47
|
+
}
|
|
48
|
+
return state;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error?.code === 'ENOENT') return null;
|
|
51
|
+
throw new ReleaseError(GATE_FAILED, `cannot read ship state: ${error.message}`, { statePath: path });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function defaultDependencies() {
|
|
56
|
+
const [
|
|
57
|
+
configModule,
|
|
58
|
+
prepareModule,
|
|
59
|
+
approveModule,
|
|
60
|
+
publishModule,
|
|
61
|
+
reconcileModule,
|
|
62
|
+
verifyModule,
|
|
63
|
+
transportModule,
|
|
64
|
+
metadataModule,
|
|
65
|
+
] = await Promise.all([
|
|
66
|
+
import('../core/config.mjs'),
|
|
67
|
+
import('./prepare.mjs'),
|
|
68
|
+
import('./approve.mjs'),
|
|
69
|
+
import('./publish.mjs'),
|
|
70
|
+
import('./reconcile.mjs'),
|
|
71
|
+
import('./verify.mjs'),
|
|
72
|
+
import('../core/git-transport.mjs'),
|
|
73
|
+
import('../core/release-metadata.mjs'),
|
|
74
|
+
]);
|
|
75
|
+
return {
|
|
76
|
+
loadProjectConfig: configModule.loadProjectConfig,
|
|
77
|
+
prepareRelease: prepareModule.prepareRelease,
|
|
78
|
+
approvePlan: approveModule.approvePlan,
|
|
79
|
+
publishRelease: publishModule.publishRelease,
|
|
80
|
+
reconcileRelease: reconcileModule.reconcileRelease,
|
|
81
|
+
verifyRelease: verifyModule.verifyRelease,
|
|
82
|
+
preflightGitTransports: transportModule.preflightGitTransports,
|
|
83
|
+
updatePreviousPublicBaselines: metadataModule.updatePreviousPublicBaselines,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function publicState(state) {
|
|
88
|
+
return {
|
|
89
|
+
command: 'ship',
|
|
90
|
+
status: state.status,
|
|
91
|
+
statePath: state.statePath,
|
|
92
|
+
targetVersion: state.targetVersion,
|
|
93
|
+
...(state.hooks && state.hooks.length > 0 ? {
|
|
94
|
+
hooks: state.hooks,
|
|
95
|
+
} : {}),
|
|
96
|
+
...(state.planPath ? {
|
|
97
|
+
planPath: state.planPath,
|
|
98
|
+
planDigest: state.planDigest,
|
|
99
|
+
evidenceDir: state.evidenceDir,
|
|
100
|
+
warnings: state.warnings ?? [],
|
|
101
|
+
} : {}),
|
|
102
|
+
...(state.approvalSummary ? { approvalSummary: state.approvalSummary } : {}),
|
|
103
|
+
...(state.approvalPath ? { approvalPath: state.approvalPath } : {}),
|
|
104
|
+
...(state.sourceRunPath ? { sourceRunPath: state.sourceRunPath } : {}),
|
|
105
|
+
...(state.requirements ? { requirements: state.requirements } : {}),
|
|
106
|
+
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
107
|
+
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
108
|
+
verificationGateAuthorizationIncludedInPlanApproval: true,
|
|
109
|
+
postVerifyMetadataUpdateIncludedInPlanApproval: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build a human-readable approval summary from the frozen plan.
|
|
115
|
+
* Lists each unit's version and every external action's id/type/unitId.
|
|
116
|
+
* Returns an empty summary when the plan file is not yet available (e.g., in tests).
|
|
117
|
+
*
|
|
118
|
+
* @param {string} planPath - Path to the frozen release plan.
|
|
119
|
+
* @returns {Promise<object>} The approval summary.
|
|
120
|
+
*/
|
|
121
|
+
async function buildApprovalSummary(planPath) {
|
|
122
|
+
try {
|
|
123
|
+
const plan = JSON.parse(await readFile(planPath, 'utf8'));
|
|
124
|
+
const units = (plan.units ?? []).map((unit) => ({
|
|
125
|
+
id: unit.id,
|
|
126
|
+
targetVersion: unit.targetVersion ?? unit.version,
|
|
127
|
+
}));
|
|
128
|
+
const actions = (plan.externalActions ?? []).map((action) => ({
|
|
129
|
+
id: action.id,
|
|
130
|
+
type: action.type,
|
|
131
|
+
unitId: action.unitId,
|
|
132
|
+
}));
|
|
133
|
+
return { units, actions };
|
|
134
|
+
} catch {
|
|
135
|
+
return { units: [], actions: [] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Advance one durable production release. Re-running is safe: the state file
|
|
141
|
+
* carries the immutable plan, approval and source-run paths so the command
|
|
142
|
+
* resumes instead of reconstructing authority from terminal/chat output.
|
|
143
|
+
*
|
|
144
|
+
* New flow (v0.4+): ship directly runs configured hooks and verification gates
|
|
145
|
+
* without a separate hook authorization step. The only human gate is plan
|
|
146
|
+
* approval. Kimi/CodeBuddy installations are non-blocking manual follow-up
|
|
147
|
+
* tasks when the plan declares humanConsumersStrategy: 'manualFollowUps'.
|
|
148
|
+
*/
|
|
149
|
+
export async function advanceShip(options = {}, injected = {}) {
|
|
150
|
+
const root = resolve(options.root ?? process.cwd());
|
|
151
|
+
const statePath = resolve(
|
|
152
|
+
options.statePath ?? resolve(root, '.release-skill', 'ships', 'current.json'),
|
|
153
|
+
);
|
|
154
|
+
const deps = Object.keys(injected).length > 0
|
|
155
|
+
? injected
|
|
156
|
+
: await defaultDependencies();
|
|
157
|
+
let state = await readState(statePath);
|
|
158
|
+
if (state?.gitTransport) {
|
|
159
|
+
process.env.RELEASE_SKILL_GIT_TRANSPORT = state.gitTransport;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!state) {
|
|
163
|
+
const loaded = await deps.loadProjectConfig({ root });
|
|
164
|
+
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
165
|
+
state = {
|
|
166
|
+
schemaVersion: 2,
|
|
167
|
+
root,
|
|
168
|
+
statePath,
|
|
169
|
+
targetVersion: options.targetVersion ?? null,
|
|
170
|
+
configDigest: loaded.configDigest,
|
|
171
|
+
hooks,
|
|
172
|
+
status: 'NEW',
|
|
173
|
+
createdAt: new Date().toISOString(),
|
|
174
|
+
updatedAt: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
await writeJsonAtomic(statePath, state);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Backward compatibility: auto-recover from legacy NEEDS_HOOK_AUTHORIZATION
|
|
180
|
+
// state. Old state files may have this status from the previous two-gate
|
|
181
|
+
// flow; the new flow runs hooks directly so we advance to NEW immediately.
|
|
182
|
+
if (state.status === 'NEEDS_HOOK_AUTHORIZATION') {
|
|
183
|
+
state.status = 'NEW';
|
|
184
|
+
state.updatedAt = new Date().toISOString();
|
|
185
|
+
await writeJsonAtomic(statePath, state);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (state.status === 'NEW') {
|
|
189
|
+
const loaded = await deps.loadProjectConfig({ root });
|
|
190
|
+
if (loaded.configDigest !== state.configDigest) {
|
|
191
|
+
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
192
|
+
state = {
|
|
193
|
+
...state,
|
|
194
|
+
configDigest: loaded.configDigest,
|
|
195
|
+
hooks,
|
|
196
|
+
status: 'NEW',
|
|
197
|
+
updatedAt: new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
await writeJsonAtomic(statePath, state);
|
|
200
|
+
}
|
|
201
|
+
const prepared = await deps.prepareRelease({
|
|
202
|
+
root,
|
|
203
|
+
version: state.targetVersion ?? undefined,
|
|
204
|
+
offline: false,
|
|
205
|
+
production: true,
|
|
206
|
+
hooksAuthorized: true,
|
|
207
|
+
verificationGatesAuthorized: true,
|
|
208
|
+
hookCache: true,
|
|
209
|
+
});
|
|
210
|
+
let transportPreflight = null;
|
|
211
|
+
if (deps.preflightGitTransports) {
|
|
212
|
+
const frozenPlan = JSON.parse(await readFile(prepared.planPath, 'utf8'));
|
|
213
|
+
transportPreflight = await deps.preflightGitTransports(frozenPlan);
|
|
214
|
+
process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
|
|
215
|
+
}
|
|
216
|
+
const approvalSummary = await buildApprovalSummary(prepared.planPath);
|
|
217
|
+
state = {
|
|
218
|
+
...state,
|
|
219
|
+
status: 'NEEDS_PLAN_APPROVAL',
|
|
220
|
+
planPath: prepared.planPath,
|
|
221
|
+
planDigest: prepared.planDigest,
|
|
222
|
+
evidenceDir: prepared.evidenceDir,
|
|
223
|
+
warnings: prepared.warnings,
|
|
224
|
+
approvalSummary,
|
|
225
|
+
...(transportPreflight ? {
|
|
226
|
+
gitTransport: transportPreflight.transport,
|
|
227
|
+
gitTransportPreflight: transportPreflight.repositories,
|
|
228
|
+
} : {}),
|
|
229
|
+
updatedAt: new Date().toISOString(),
|
|
230
|
+
};
|
|
231
|
+
await writeJsonAtomic(statePath, state);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (state.status === 'NEEDS_PLAN_APPROVAL') {
|
|
235
|
+
// Boolean --approve: auto-use the stored planDigest (no user digest input needed).
|
|
236
|
+
// Legacy --approve-plan <digest>: explicit digest must match.
|
|
237
|
+
const approveRequested = options.approve === true
|
|
238
|
+
|| (typeof options.planApprovalDigest === 'string' && options.planApprovalDigest.length > 0);
|
|
239
|
+
if (!approveRequested) return publicState(state);
|
|
240
|
+
if (options.approve === true) {
|
|
241
|
+
// Boolean approve: no digest input; the stored planDigest is authoritative.
|
|
242
|
+
} else if (options.planApprovalDigest !== state.planDigest) {
|
|
243
|
+
throw new ReleaseError(PLAN_DIGEST_MISMATCH, 'ship plan approval digest does not match the frozen plan');
|
|
244
|
+
}
|
|
245
|
+
if (!options.actor) {
|
|
246
|
+
throw new ReleaseError(GATE_FAILED, 'ship plan approval requires --actor <person>');
|
|
247
|
+
}
|
|
248
|
+
const approval = await deps.approvePlan({
|
|
249
|
+
planPath: state.planPath,
|
|
250
|
+
expectedDigest: state.planDigest,
|
|
251
|
+
actor: options.actor,
|
|
252
|
+
});
|
|
253
|
+
state = {
|
|
254
|
+
...state,
|
|
255
|
+
status: 'APPROVED',
|
|
256
|
+
approvalPath: approval.approvalPath,
|
|
257
|
+
verificationGatesAuthorized: true,
|
|
258
|
+
approvedBy: options.actor,
|
|
259
|
+
updatedAt: new Date().toISOString(),
|
|
260
|
+
};
|
|
261
|
+
await writeJsonAtomic(statePath, state);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (state.status === 'APPROVED' || state.status === 'PUBLISHING') {
|
|
265
|
+
if (!options.adapterRegistry) {
|
|
266
|
+
throw new ReleaseError(GATE_FAILED, 'ship requires an adapter registry to publish');
|
|
267
|
+
}
|
|
268
|
+
state.status = 'PUBLISHING';
|
|
269
|
+
await writeJsonAtomic(statePath, state);
|
|
270
|
+
const published = await deps.publishRelease({
|
|
271
|
+
planPath: state.planPath,
|
|
272
|
+
approvalPath: state.approvalPath,
|
|
273
|
+
adapterRegistry: options.adapterRegistry,
|
|
274
|
+
root,
|
|
275
|
+
productionMode: true,
|
|
276
|
+
});
|
|
277
|
+
state = {
|
|
278
|
+
...state,
|
|
279
|
+
status: published.status,
|
|
280
|
+
sourceRunPath: published.runPath,
|
|
281
|
+
updatedAt: new Date().toISOString(),
|
|
282
|
+
};
|
|
283
|
+
await writeJsonAtomic(statePath, state);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (state.status === 'PARTIAL') {
|
|
287
|
+
const reconciled = await deps.reconcileRelease({
|
|
288
|
+
planPath: state.planPath,
|
|
289
|
+
sourceRunPath: state.sourceRunPath,
|
|
290
|
+
approvalPath: state.approvalPath,
|
|
291
|
+
adapterRegistry: options.adapterRegistry,
|
|
292
|
+
root,
|
|
293
|
+
});
|
|
294
|
+
state = {
|
|
295
|
+
...state,
|
|
296
|
+
status: reconciled.status,
|
|
297
|
+
sourceRunPath: reconciled.runPath,
|
|
298
|
+
updatedAt: new Date().toISOString(),
|
|
299
|
+
};
|
|
300
|
+
await writeJsonAtomic(statePath, state);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (state.status === 'PUBLISHED' || state.status === 'NEEDS_MANUAL_ATTESTATIONS') {
|
|
304
|
+
try {
|
|
305
|
+
const verified = await deps.verifyRelease({
|
|
306
|
+
planPath: state.planPath,
|
|
307
|
+
sourceRunPath: state.sourceRunPath,
|
|
308
|
+
adapterRegistry: options.adapterRegistry,
|
|
309
|
+
root,
|
|
310
|
+
verificationGatesAuthorized: state.verificationGatesAuthorized === true,
|
|
311
|
+
});
|
|
312
|
+
state = {
|
|
313
|
+
...state,
|
|
314
|
+
status: verified.status,
|
|
315
|
+
verifyRunPath: verified.runPath,
|
|
316
|
+
requirements: undefined,
|
|
317
|
+
manualFollowUps: verified.manualFollowUps ?? undefined,
|
|
318
|
+
updatedAt: new Date().toISOString(),
|
|
319
|
+
};
|
|
320
|
+
await writeJsonAtomic(statePath, state);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error?.code !== CONSUMER_VERIFICATION_DEFERRED) throw error;
|
|
323
|
+
state = {
|
|
324
|
+
...state,
|
|
325
|
+
status: 'NEEDS_MANUAL_ATTESTATIONS',
|
|
326
|
+
requirements: error.details?.requirements ?? [],
|
|
327
|
+
updatedAt: new Date().toISOString(),
|
|
328
|
+
};
|
|
329
|
+
await writeJsonAtomic(statePath, state);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return publicState(state);
|
|
334
|
+
}
|
package/src/commands/verify.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
GATE_FAILED,
|
|
44
44
|
CONFIG_MISSING,
|
|
45
45
|
POST_PUBLISH_VERIFY_FAILED,
|
|
46
|
+
CONSUMER_VERIFICATION_DEFERRED,
|
|
46
47
|
} from '../core/errors.mjs';
|
|
47
48
|
import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
|
|
48
49
|
import { assertTransition, PUBLISHED, VERIFIED } from '../core/state-machine.mjs';
|
|
@@ -67,6 +68,20 @@ import {
|
|
|
67
68
|
buildDirectoryFileIndex,
|
|
68
69
|
checkNpmEntryClosure,
|
|
69
70
|
} from '../npm/npm-entry-closure.mjs';
|
|
71
|
+
import {
|
|
72
|
+
KIMI_ATTESTATION_FILE,
|
|
73
|
+
KIMI_REQUIREMENT_FILE,
|
|
74
|
+
kimiAuthorityDir,
|
|
75
|
+
resolveBoundPlanDigest,
|
|
76
|
+
validateKimiAttestation,
|
|
77
|
+
} from '../platforms/kimi.mjs';
|
|
78
|
+
import {
|
|
79
|
+
CODEBUDDY_ATTESTATION_FILE,
|
|
80
|
+
CODEBUDDY_REQUIREMENT_FILE,
|
|
81
|
+
codebuddyAuthorityDir,
|
|
82
|
+
resolveCodeBuddyBoundPlanDigest,
|
|
83
|
+
validateCodeBuddyAttestation,
|
|
84
|
+
} from '../platforms/codebuddy.mjs';
|
|
70
85
|
|
|
71
86
|
// ---------------------------------------------------------------------------
|
|
72
87
|
// Constants
|
|
@@ -120,6 +135,103 @@ function isValidDigest(digest) {
|
|
|
120
135
|
return typeof digest === 'string' && /^[a-f0-9]{64}$/.test(digest);
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Generate every manual consumer requirement before any expensive automatic
|
|
140
|
+
* consumer install starts. This turns a sequence of "install Kimi, rerun,
|
|
141
|
+
* then discover CodeBuddy" failures into one actionable response.
|
|
142
|
+
*/
|
|
143
|
+
async function collectMissingManualAttestations({
|
|
144
|
+
actions,
|
|
145
|
+
adapterRegistry,
|
|
146
|
+
plan,
|
|
147
|
+
root,
|
|
148
|
+
runDir,
|
|
149
|
+
clockFn,
|
|
150
|
+
}) {
|
|
151
|
+
const manual = actions.filter((action) => (
|
|
152
|
+
action.type === 'kimi-marketplace-install'
|
|
153
|
+
|| action.type === 'codebuddy-marketplace-install'
|
|
154
|
+
));
|
|
155
|
+
if (manual.length === 0) return [];
|
|
156
|
+
|
|
157
|
+
const missing = [];
|
|
158
|
+
for (const action of manual) {
|
|
159
|
+
const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
|
|
160
|
+
const adapter = adapterRegistry.getAdapter(adapterActionType);
|
|
161
|
+
// Tests and embedders may intentionally register a fully automatic
|
|
162
|
+
// adapter for these action types. Only the real platform adapter owns the
|
|
163
|
+
// interactive manual-install protocol and its stable authority files.
|
|
164
|
+
if (adapter.name !== 'plugin-marketplace') continue;
|
|
165
|
+
const context = {
|
|
166
|
+
externalWritesAuthorized: false,
|
|
167
|
+
isolatedConsumerWritesAuthorized: true,
|
|
168
|
+
plan,
|
|
169
|
+
baseline: plan.baseline,
|
|
170
|
+
root,
|
|
171
|
+
runDir,
|
|
172
|
+
};
|
|
173
|
+
const actionInput = { actionType: adapterActionType, ...action.parameters };
|
|
174
|
+
const preflight = await adapter.preflight(actionInput, context);
|
|
175
|
+
if (preflight.status !== 'PREFLIGHT_PASSED') {
|
|
176
|
+
throw new ReleaseError(
|
|
177
|
+
POST_PUBLISH_VERIFY_FAILED,
|
|
178
|
+
`manual consumer preflight did not pass for action "${action.id}": ${preflight.error}`,
|
|
179
|
+
{ actionId: action.id, platform: action.type.split('-')[0] },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const executed = await adapter.execute(actionInput, context);
|
|
183
|
+
if (executed.status !== 'EXECUTED') {
|
|
184
|
+
throw new ReleaseError(
|
|
185
|
+
POST_PUBLISH_VERIFY_FAILED,
|
|
186
|
+
`cannot generate manual consumer requirement for action "${action.id}": ${executed.error}`,
|
|
187
|
+
{ actionId: action.id, platform: action.type.split('-')[0] },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const isKimi = action.type === 'kimi-marketplace-install';
|
|
192
|
+
const planDigest = isKimi
|
|
193
|
+
? resolveBoundPlanDigest(context)
|
|
194
|
+
: await resolveCodeBuddyBoundPlanDigest(context);
|
|
195
|
+
const authorityDir = isKimi
|
|
196
|
+
? kimiAuthorityDir(context, planDigest, action.parameters.plugin)
|
|
197
|
+
: codebuddyAuthorityDir(context, planDigest, action.parameters.plugin);
|
|
198
|
+
const requirementPath = join(
|
|
199
|
+
authorityDir,
|
|
200
|
+
isKimi ? KIMI_REQUIREMENT_FILE : CODEBUDDY_REQUIREMENT_FILE,
|
|
201
|
+
);
|
|
202
|
+
const attestationPath = join(
|
|
203
|
+
authorityDir,
|
|
204
|
+
isKimi ? KIMI_ATTESTATION_FILE : CODEBUDDY_ATTESTATION_FILE,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
let valid = false;
|
|
208
|
+
let reason = 'attestation file is missing';
|
|
209
|
+
try {
|
|
210
|
+
const attestation = JSON.parse(await readFile(attestationPath, 'utf8'));
|
|
211
|
+
const validation = isKimi
|
|
212
|
+
? validateKimiAttestation(attestation, action.parameters, clockFn(), planDigest)
|
|
213
|
+
: validateCodeBuddyAttestation(attestation, action.parameters, clockFn(), planDigest);
|
|
214
|
+
valid = validation.valid;
|
|
215
|
+
reason = validation.error;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error?.code !== 'ENOENT') reason = `attestation cannot be read: ${error.message}`;
|
|
218
|
+
}
|
|
219
|
+
if (!valid) {
|
|
220
|
+
missing.push({
|
|
221
|
+
actionId: action.id,
|
|
222
|
+
platform: isKimi ? 'kimi' : 'codebuddy',
|
|
223
|
+
plugin: action.parameters.plugin,
|
|
224
|
+
version: action.parameters.version,
|
|
225
|
+
planDigest,
|
|
226
|
+
requirementPath,
|
|
227
|
+
attestationPath,
|
|
228
|
+
reason,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return missing;
|
|
233
|
+
}
|
|
234
|
+
|
|
123
235
|
// ---------------------------------------------------------------------------
|
|
124
236
|
// Smoke test
|
|
125
237
|
// ---------------------------------------------------------------------------
|
|
@@ -643,7 +755,7 @@ export async function verifyRelease(options) {
|
|
|
643
755
|
runDir: runDirOpt,
|
|
644
756
|
clock: clockOpt,
|
|
645
757
|
npmExecutor,
|
|
646
|
-
verificationGatesAuthorized,
|
|
758
|
+
verificationGatesAuthorized: _verificationGatesAuthorized,
|
|
647
759
|
gateEnv,
|
|
648
760
|
previousVerifyRun,
|
|
649
761
|
} = options ?? {};
|
|
@@ -714,16 +826,9 @@ export async function verifyRelease(options) {
|
|
|
714
826
|
.filter((distribution) => distribution.type === 'npm' && distribution.smokeBin)
|
|
715
827
|
.map((distribution) => ({ unitId: unit.id, smokeBin: distribution.smokeBin }))
|
|
716
828
|
));
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
`plan declares ${consumerGates.length} consumer verification gate(s) and ` +
|
|
721
|
-
`${configuredSmokeBins.length} npm CLI smoke process(es). ` +
|
|
722
|
-
'They execute installed project code without an OS or network sandbox. ' +
|
|
723
|
-
'To proceed, pass --acknowledge-gate-side-effects (CLI) or verificationGatesAuthorized=true (API).',
|
|
724
|
-
{ gateIds: consumerGates.map((gate) => gate.id), configuredSmokeBins },
|
|
725
|
-
);
|
|
726
|
-
}
|
|
829
|
+
// The command invocation itself authorizes execution of configured
|
|
830
|
+
// verification gates and smoke processes. Old --acknowledge-gate-side-effects
|
|
831
|
+
// is accepted as a no-effect compatibility input.
|
|
727
832
|
|
|
728
833
|
await evidence.append({ phase: 'verify', step: 'plan-load', status: 'passed' });
|
|
729
834
|
|
|
@@ -1000,7 +1105,43 @@ export async function verifyRelease(options) {
|
|
|
1000
1105
|
}
|
|
1001
1106
|
}
|
|
1002
1107
|
|
|
1003
|
-
|
|
1108
|
+
// When the plan declares humanConsumersStrategy: 'manualFollowUps',
|
|
1109
|
+
// Kimi/CodeBuddy installations are non-blocking post-release manual tasks.
|
|
1110
|
+
// Skip attestation collection and blocking for these platforms.
|
|
1111
|
+
const isHumanConsumerFollowUp = plan.humanConsumersStrategy === 'manualFollowUps';
|
|
1112
|
+
|
|
1113
|
+
const missingManualAttestations = isHumanConsumerFollowUp
|
|
1114
|
+
? []
|
|
1115
|
+
: await collectMissingManualAttestations({
|
|
1116
|
+
actions,
|
|
1117
|
+
adapterRegistry,
|
|
1118
|
+
plan,
|
|
1119
|
+
root,
|
|
1120
|
+
runDir,
|
|
1121
|
+
clockFn,
|
|
1122
|
+
});
|
|
1123
|
+
if (missingManualAttestations.length > 0) {
|
|
1124
|
+
await evidence.append({
|
|
1125
|
+
phase: 'verify',
|
|
1126
|
+
step: 'manual-attestations',
|
|
1127
|
+
status: 'needs-input',
|
|
1128
|
+
requirements: missingManualAttestations,
|
|
1129
|
+
});
|
|
1130
|
+
throw new ReleaseError(
|
|
1131
|
+
CONSUMER_VERIFICATION_DEFERRED,
|
|
1132
|
+
`${missingManualAttestations.length} manual consumer attestation(s) are required`,
|
|
1133
|
+
{ requirements: missingManualAttestations },
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Collect non-blocking manual follow-up tasks for human consumer platforms
|
|
1138
|
+
const manualFollowUps = [];
|
|
1139
|
+
|
|
1140
|
+
// Every action is identity-bound and adapters receive read-only or
|
|
1141
|
+
// per-action isolated consumer paths. Run independent checks concurrently;
|
|
1142
|
+
// the evidence writer serializes append operations and result arrays are
|
|
1143
|
+
// sorted afterwards for deterministic receipts.
|
|
1144
|
+
const actionResults = await Promise.allSettled(actions.map(async (action) => {
|
|
1004
1145
|
const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
|
|
1005
1146
|
|
|
1006
1147
|
// Skip meta-checkpoints
|
|
@@ -1011,7 +1152,7 @@ export async function verifyRelease(options) {
|
|
|
1011
1152
|
status: 'SKIPPED',
|
|
1012
1153
|
reason: 'meta-checkpoint',
|
|
1013
1154
|
});
|
|
1014
|
-
|
|
1155
|
+
return;
|
|
1015
1156
|
}
|
|
1016
1157
|
|
|
1017
1158
|
let adapter;
|
|
@@ -1027,6 +1168,38 @@ export async function verifyRelease(options) {
|
|
|
1027
1168
|
}
|
|
1028
1169
|
|
|
1029
1170
|
if (isMarketplaceAction(action.type)) {
|
|
1171
|
+
// When humanConsumersStrategy is 'manualFollowUps', Kimi/CodeBuddy
|
|
1172
|
+
// installations are non-blocking manual follow-up tasks. Skip adapter
|
|
1173
|
+
// processing and collect them as manualFollowUps.
|
|
1174
|
+
const isHumanConsumerPlatform = action.type === 'kimi-marketplace-install'
|
|
1175
|
+
|| action.type === 'codebuddy-marketplace-install';
|
|
1176
|
+
if (isHumanConsumerFollowUp && isHumanConsumerPlatform) {
|
|
1177
|
+
const platform = action.type === 'kimi-marketplace-install' ? 'kimi' : 'codebuddy';
|
|
1178
|
+
manualFollowUps.push({
|
|
1179
|
+
actionId: action.id,
|
|
1180
|
+
platform,
|
|
1181
|
+
plugin: action.parameters?.plugin,
|
|
1182
|
+
version: action.parameters?.version,
|
|
1183
|
+
unitId: action.unitId,
|
|
1184
|
+
verifiedBySystem: false,
|
|
1185
|
+
reason: 'human consumer platform — installation is a manual post-release task; not verified by system',
|
|
1186
|
+
});
|
|
1187
|
+
adapterChecks.push({
|
|
1188
|
+
actionId: action.id,
|
|
1189
|
+
actionType: action.type,
|
|
1190
|
+
status: 'SKIPPED',
|
|
1191
|
+
reason: 'manual-follow-up',
|
|
1192
|
+
});
|
|
1193
|
+
await evidence.append({
|
|
1194
|
+
phase: 'verify-marketplace',
|
|
1195
|
+
actionId: action.id,
|
|
1196
|
+
actionType: action.type,
|
|
1197
|
+
status: 'SKIPPED',
|
|
1198
|
+
reason: 'manual-follow-up',
|
|
1199
|
+
});
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1030
1203
|
// --- Marketplace: fresh consumer verification in verify's own runDir ---
|
|
1031
1204
|
// Context: isolatedConsumerWritesAuthorized allows writing to verify's
|
|
1032
1205
|
// runDir/consumers/ directory; externalWritesAuthorized stays false.
|
|
@@ -1142,7 +1315,7 @@ export async function verifyRelease(options) {
|
|
|
1142
1315
|
installationContractDigest: currentDigest,
|
|
1143
1316
|
});
|
|
1144
1317
|
|
|
1145
|
-
|
|
1318
|
+
return;
|
|
1146
1319
|
}
|
|
1147
1320
|
}
|
|
1148
1321
|
|
|
@@ -1305,7 +1478,12 @@ export async function verifyRelease(options) {
|
|
|
1305
1478
|
);
|
|
1306
1479
|
}
|
|
1307
1480
|
}
|
|
1308
|
-
}
|
|
1481
|
+
}));
|
|
1482
|
+
adapterChecks.sort((a, b) => a.actionId.localeCompare(b.actionId));
|
|
1483
|
+
consumerVerificationReceipts.sort((a, b) => a.actionId.localeCompare(b.actionId));
|
|
1484
|
+
consumerGateResults.sort((a, b) => a.id.localeCompare(b.id));
|
|
1485
|
+
const rejectedAction = actionResults.find((result) => result.status === 'rejected');
|
|
1486
|
+
if (rejectedAction) throw rejectedAction.reason;
|
|
1309
1487
|
|
|
1310
1488
|
await evidence.append({ phase: 'verify', step: 'adapter-verify', status: 'completed' });
|
|
1311
1489
|
|
|
@@ -1494,6 +1672,7 @@ export async function verifyRelease(options) {
|
|
|
1494
1672
|
gateResults: consumerGateResults,
|
|
1495
1673
|
consumerVerificationReceipts,
|
|
1496
1674
|
skillResourceClosureReceipts,
|
|
1675
|
+
...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
|
|
1497
1676
|
startedAt: clockFn(),
|
|
1498
1677
|
finishedAt: clockFn(),
|
|
1499
1678
|
};
|
|
@@ -1517,6 +1696,7 @@ export async function verifyRelease(options) {
|
|
|
1517
1696
|
adapterChecks,
|
|
1518
1697
|
smokeTest,
|
|
1519
1698
|
gateResults: consumerGateResults,
|
|
1699
|
+
...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
|
|
1520
1700
|
};
|
|
1521
1701
|
} catch (err) {
|
|
1522
1702
|
await evidence.append({
|