release-skill 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +22 -0
  7. package/CONTRIBUTING.md +27 -0
  8. package/INSTALL.md +95 -139
  9. package/INSTALL.zh-CN.md +70 -121
  10. package/README.md +264 -913
  11. package/README.zh-CN.md +222 -535
  12. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  13. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  14. package/adapters/claude/bin/release-skill.bundle.mjs +19054 -17573
  15. package/adapters/claude/schemas/release-plan.schema.json +137 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +70 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +19054 -17573
  20. package/adapters/codex/schemas/release-plan.schema.json +137 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +70 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +19054 -17573
  25. package/adapters/kimi/schemas/release-plan.schema.json +137 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +70 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +19054 -17573
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
  33. package/bin/release-skill.bundle.mjs +19054 -17573
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +137 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +70 -2
  38. package/src/adapters/plugin-marketplace.mjs +1190 -435
  39. package/src/commands/prepare.mjs +380 -40
  40. package/src/commands/publish.mjs +107 -75
  41. package/src/commands/reconcile.mjs +92 -327
  42. package/src/commands/setup.mjs +148 -20
  43. package/src/commands/verify.mjs +304 -20
  44. package/src/core/baseline.mjs +8 -1
  45. package/src/core/checkpoints.mjs +50 -7
  46. package/src/core/config.mjs +15 -0
  47. package/src/core/errors.mjs +2 -0
  48. package/src/core/installation-contract.mjs +341 -0
  49. package/src/core/plan.mjs +129 -6
  50. package/src/platforms/codebuddy.mjs +193 -280
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +24 -6
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Codex platform human-attestation fallback strategy.
3
+ *
4
+ * Codex is primarily an automatable platform (structured-cli), but its
5
+ * degradationPolicy is 'human-attestation-with-fallback': when the CLI
6
+ * interface, environment, or transport is unavailable, the system falls back
7
+ * to a human attestation path. Explicit mismatches (identity, version,
8
+ * payload, marketplace entry) are hard failures and never trigger fallback.
9
+ *
10
+ * 统一人工结果:收据仅需 platform, version, planDigest,
11
+ * result(passed|failed), actor, confirmedAt 和可选 note。
12
+ * 不再要求 consumer, plugin, conclusion, confirmedBy、隔离 HOME、
13
+ * 安装路径证明、载荷摘要手填或 24 小时过期。
14
+ *
15
+ * @module platforms/codex
16
+ */
17
+
18
+ import { readFile } from 'node:fs/promises';
19
+ import { join, resolve, relative, isAbsolute } from 'node:path';
20
+
21
+ import {
22
+ ActionType,
23
+ ActionStatus,
24
+ createResult,
25
+ resolveTimeoutMs,
26
+ SAFE_ID_RE,
27
+ writeEvidenceAtomic,
28
+ } from '../adapters/contract.mjs';
29
+ import { canonicalJson } from '../core/digest.mjs';
30
+
31
+ /** 64-char lowercase hex plan/payload digest pattern. */
32
+ const HEX_DIGEST_RE = /^[a-f0-9]{64}$/;
33
+
34
+ /**
35
+ * Normalize a plan back to its frozen form for digest comparison.
36
+ * Only lifecycle status fields are reset: the top-level `status` returns to
37
+ * "PREPARED" and every `externalActions[].status` returns to "PENDING".
38
+ *
39
+ * @param {object} plan
40
+ * @returns {object} the lifecycle-normalized plan
41
+ */
42
+ function normalizePlanForDigest(plan) {
43
+ const normalized = { ...plan, status: 'PREPARED' };
44
+ if (Array.isArray(plan.externalActions)) {
45
+ normalized.externalActions = plan.externalActions.map((action) => (
46
+ action && typeof action === 'object' && !Array.isArray(action)
47
+ ? { ...action, status: 'PENDING' }
48
+ : action
49
+ ));
50
+ }
51
+ return normalized;
52
+ }
53
+
54
+ /** Structured manual-install requirement written by codex fallback execute. */
55
+ export const CODEX_REQUIREMENT_FILE = 'release-skill-codex-manual-install.json';
56
+ /** Structured human attestation consumed by codex fallback observe. */
57
+ export const CODEX_ATTESTATION_FILE = 'release-skill-codex-attestation.json';
58
+
59
+ /**
60
+ * Resolve and verify the genuine frozen plan digest from the adapter context,
61
+ * for the codex human-attestation fallback path.
62
+ *
63
+ * @param {object} context - adapter context (must carry the frozen `plan`).
64
+ * @returns {Promise<string>} the verified frozen plan digest.
65
+ * @throws {Error} when the plan is absent or the digest does not match.
66
+ */
67
+ export async function resolveCodexBoundPlanDigest(context) {
68
+ const plan = context?.plan;
69
+ if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
70
+ throw new Error('context.plan is required to bind the codex plan digest');
71
+ }
72
+ const carried = plan.digest;
73
+ if (typeof carried !== 'string' || !HEX_DIGEST_RE.test(carried)) {
74
+ throw new Error('context.plan.digest must be a 64-char lowercase hex frozen plan digest');
75
+ }
76
+ // Lazy import keeps the static bundle graph acyclic.
77
+ const { computePlanDigest } = await import('../core/plan.mjs');
78
+ const normalized = normalizePlanForDigest(plan);
79
+ if (computePlanDigest(normalized) !== carried) {
80
+ throw new Error('context.plan.digest does not match the normalized frozen plan (a non-lifecycle field was tampered)');
81
+ }
82
+ return carried;
83
+ }
84
+
85
+ /**
86
+ * Authoritative, cross-run attestation directory for a codex fallback install.
87
+ *
88
+ * Lives at a stable root-fixed location keyed by the verified frozen plan
89
+ * digest and plugin id:
90
+ * <root>/.release-skill/codex-attestations/<planDigest>/<plugin>/
91
+ *
92
+ * @param {object} context - adapter context (needs `root`).
93
+ * @param {string} planDigest - verified frozen plan digest (64-hex).
94
+ * @param {string} plugin - plugin id (SAFE_ID_RE).
95
+ * @returns {string} absolute authority directory.
96
+ */
97
+ export function codexAuthorityDir(context, planDigest, plugin) {
98
+ if (!context?.root) {
99
+ throw new Error('context.root is required for the codex attestation authority');
100
+ }
101
+ if (!HEX_DIGEST_RE.test(planDigest)) {
102
+ throw new Error('codex attestation authority requires a 64-hex plan digest');
103
+ }
104
+ if (!SAFE_ID_RE.test(plugin)) {
105
+ throw new Error(`codex attestation authority requires a safe plugin id: "${plugin}"`);
106
+ }
107
+ const base = resolve(context.root, '.release-skill', 'codex-attestations');
108
+ const dir = resolve(base, planDigest, plugin);
109
+ const rel = relative(base, dir);
110
+ const sep = process.platform === 'win32' ? '\\' : '/';
111
+ if (
112
+ rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)
113
+ || rel.split(sep).some((segment) => segment === '..' || segment === '')
114
+ ) {
115
+ throw new Error('codex attestation authority path escapes its base');
116
+ }
117
+ return dir;
118
+ }
119
+
120
+ /**
121
+ * 统一人工结果验证:验证 codex 人工结果是否匹配冻结计划。
122
+ *
123
+ * 统一后的结果只需:
124
+ * - 必填:platform, version, planDigest, result(passed|failed), actor, confirmedAt
125
+ * - 可选:note
126
+ *
127
+ * 绑定验证(任何不匹配都失败):
128
+ * - planDigest 绑定到真正的冻结计划摘要(boundPlanDigest)
129
+ * - version 静态一致性检查
130
+ * - result 只接受 passed 或 failed
131
+ *
132
+ * @param {object} attestation - 解析后的人工结果 JSON。
133
+ * @param {object} action - 展开的 codex 动作(顶层字段)。
134
+ * @param {string} isoNow - 当前 ISO 时间戳(保留签名兼容,不再用于过期检查)。
135
+ * @param {string} boundPlanDigest - 验证过的冻结计划摘要。
136
+ * @returns {{valid:boolean, error:string|null}}
137
+ */
138
+ export function validateCodexAttestation(attestation, action, isoNow, boundPlanDigest) {
139
+ if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
140
+ return { valid: false, error: 'codex attestation is not an object' };
141
+ }
142
+ // 统一必填字段
143
+ const requiredStrings = ['platform', 'version', 'planDigest', 'result', 'actor', 'confirmedAt'];
144
+ for (const field of requiredStrings) {
145
+ if (typeof attestation[field] !== 'string' || attestation[field].length === 0) {
146
+ return { valid: false, error: `codex attestation missing required field "${field}"` };
147
+ }
148
+ }
149
+ if (attestation.platform !== 'codex') {
150
+ return { valid: false, error: `codex attestation platform "${attestation.platform}" must be "codex"` };
151
+ }
152
+ // result 只接受 passed 或 failed
153
+ if (attestation.result !== 'passed' && attestation.result !== 'failed') {
154
+ return { valid: false, error: `codex attestation result "${attestation.result}" must be "passed" or "failed"` };
155
+ }
156
+ // planDigest 绑定验证
157
+ if (!HEX_DIGEST_RE.test(attestation.planDigest)) {
158
+ return { valid: false, error: 'codex attestation planDigest must be a 64-char lowercase hex digest' };
159
+ }
160
+ if (attestation.planDigest !== boundPlanDigest) {
161
+ return { valid: false, error: 'codex attestation planDigest does not match the frozen plan digest' };
162
+ }
163
+ // version 静态一致性检查
164
+ if (attestation.version !== action.version) {
165
+ return { valid: false, error: `codex attestation version "${attestation.version}" does not match action version "${action.version}"` };
166
+ }
167
+ // confirmedAt 必须是有效的时间戳
168
+ const confirmedMs = Date.parse(attestation.confirmedAt);
169
+ if (!Number.isFinite(confirmedMs)) {
170
+ return { valid: false, error: 'codex attestation confirmedAt must be a valid ISO timestamp' };
171
+ }
172
+ // 可选字段 note 如果存在必须是字符串
173
+ if (attestation.note !== undefined && typeof attestation.note !== 'string') {
174
+ return { valid: false, error: 'codex attestation note must be a string when present' };
175
+ }
176
+ return { valid: true, error: null };
177
+ }
178
+
179
+ /**
180
+ * Read the authoritative Codex plugin manifest from a verified plugin root.
181
+ *
182
+ * Single candidate `.codex-plugin/plugin.json`. Returns the parsed manifest
183
+ * and the root-relative manifest path. Throws when the manifest is absent,
184
+ * not valid JSON, or not an object.
185
+ *
186
+ * @param {string} pluginRootReal - realpath of the verified plugin root.
187
+ * @returns {Promise<{manifest:object, manifestRelative:string}>}
188
+ */
189
+ export async function readCodexManifest(pluginRootReal) {
190
+ const manifestRelative = join('.codex-plugin', 'plugin.json');
191
+ const manifestPath = resolve(pluginRootReal, manifestRelative);
192
+ let content;
193
+ try {
194
+ content = await readFile(manifestPath, 'utf8');
195
+ } catch {
196
+ throw new Error(`no codex plugin manifest found (expected ${manifestRelative})`);
197
+ }
198
+ let manifest;
199
+ try {
200
+ manifest = JSON.parse(content);
201
+ } catch {
202
+ throw new Error(`codex plugin manifest ${manifestRelative} is not valid JSON`);
203
+ }
204
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
205
+ throw new Error(`codex plugin manifest ${manifestRelative} is not an object`);
206
+ }
207
+ return { manifest, manifestRelative };
208
+ }
209
+
210
+ /**
211
+ * Codex human-attestation fallback: when the CLI interface, environment, or
212
+ * transport is unavailable, emit a manual-install requirement bound to the
213
+ * real frozen plan digest + identity. Explicit mismatches (identity, version,
214
+ * payload, marketplace entry) are hard failures and never reach this path.
215
+ *
216
+ * @param {object} action - expanded codex action (validated params already).
217
+ * @param {object} context - adapter context (root, runDir, plan).
218
+ * @returns {Promise<import('../adapters/contract.mjs').AdapterResult>}
219
+ */
220
+ export async function executeCodexManualRequirement(action, context) {
221
+ const actionType = ActionType.CODEX_MARKETPLACE_INSTALL;
222
+
223
+ // Bind to the REAL frozen plan digest via strict normalized recompute.
224
+ let planDigest;
225
+ try {
226
+ planDigest = await resolveCodexBoundPlanDigest(context);
227
+ } catch (planErr) {
228
+ return createResult({
229
+ actionType,
230
+ status: ActionStatus.EXECUTE_FAILED,
231
+ error: `cannot bind codex requirement to the frozen plan: ${planErr.message}`,
232
+ });
233
+ }
234
+
235
+ // Validate the frozen timeout.
236
+ try {
237
+ resolveTimeoutMs(action);
238
+ } catch (timeoutErr) {
239
+ return createResult({
240
+ actionType,
241
+ status: ActionStatus.EXECUTE_FAILED,
242
+ error: timeoutErr.message,
243
+ });
244
+ }
245
+
246
+ const ref = action.ref ?? `v${action.version}`;
247
+
248
+ // Stable, plan-digest-keyed authority dir.
249
+ let attestationDir;
250
+ try {
251
+ attestationDir = codexAuthorityDir(context, planDigest, action.plugin);
252
+ } catch (dirErr) {
253
+ return createResult({
254
+ actionType,
255
+ status: ActionStatus.EXECUTE_FAILED,
256
+ error: dirErr.message,
257
+ });
258
+ }
259
+
260
+ const instructions = [
261
+ `Codex CLI 不可用(接口、环境或传输问题),降级为人工结果路径。`,
262
+ `1) publish 完成所有远端写入后进入 PUBLISHED 状态(自动化 Git 分支/标签、npm 和 GitHub Release 写入已完成)。此 codex 检查点标记为需要人工安装。`,
263
+ `2) 手动安装插件 "${action.plugin}" 版本 ${action.version}(ref: ${ref})。`,
264
+ `3) 将人工结果 JSON 写入: ${attestationDir}/${CODEX_ATTESTATION_FILE}`,
265
+ ` 必填字段: platform="codex", version, planDigest(冻结计划摘要), result("passed" 或 "failed"), actor(确认人), confirmedAt(ISO 8601 时间戳)`,
266
+ ` 可选字段: note(备注)`,
267
+ `4) 运行 release-skill verify(从同一个计划摘要索引的权威目录读取结果,成功后 -> VERIFIED)。`,
268
+ ];
269
+
270
+ // 统一 requirement 结构
271
+ const requirement = {
272
+ kind: 'codex-manual-install-requirement',
273
+ platform: 'codex',
274
+ plugin: action.plugin,
275
+ version: action.version,
276
+ repo: action.repo,
277
+ ref,
278
+ entrySkill: action.entrySkill,
279
+ planDigest,
280
+ attestationDir,
281
+ attestationFile: CODEX_ATTESTATION_FILE,
282
+ attestationTemplate: {
283
+ platform: 'codex',
284
+ version: action.version,
285
+ planDigest,
286
+ result: '<"passed" or "failed">',
287
+ actor: '<person who confirmed the install>',
288
+ confirmedAt: '<ISO 8601 timestamp>',
289
+ note: '<optional note>',
290
+ },
291
+ instructions,
292
+ };
293
+
294
+ // Ensure the authority directory exists.
295
+ const { mkdir } = await import('node:fs/promises');
296
+ try {
297
+ await mkdir(attestationDir, { recursive: true, mode: 0o700 });
298
+ } catch (mkdirErr) {
299
+ if (mkdirErr?.code !== 'EEXIST') {
300
+ return createResult({
301
+ actionType,
302
+ status: ActionStatus.EXECUTE_FAILED,
303
+ error: `cannot create codex attestation directory: ${mkdirErr.message}`,
304
+ });
305
+ }
306
+ }
307
+
308
+ // Idempotent requirement write.
309
+ const requirementPath = resolve(attestationDir, CODEX_REQUIREMENT_FILE);
310
+ let existing = null;
311
+ let requirementMissing = false;
312
+ try {
313
+ const existingRaw = await readFile(requirementPath, 'utf8');
314
+ try {
315
+ existing = JSON.parse(existingRaw);
316
+ } catch (parseErr) {
317
+ return createResult({
318
+ actionType,
319
+ status: ActionStatus.EXECUTE_FAILED,
320
+ error: `existing codex manual-install requirement is invalid JSON; refusing to overwrite: ${parseErr.message}`,
321
+ });
322
+ }
323
+ } catch (readErr) {
324
+ if (readErr?.code === 'ENOENT') {
325
+ requirementMissing = true;
326
+ } else {
327
+ return createResult({
328
+ actionType,
329
+ status: ActionStatus.EXECUTE_FAILED,
330
+ error: `existing codex manual-install requirement cannot be read; refusing to overwrite: ${readErr.message}`,
331
+ });
332
+ }
333
+ }
334
+ if (!requirementMissing) {
335
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
336
+ return createResult({
337
+ actionType,
338
+ status: ActionStatus.EXECUTE_FAILED,
339
+ error: 'existing codex manual-install requirement is not an object; refusing to overwrite',
340
+ });
341
+ }
342
+ const { createdAt: _existingCreatedAt, ...existingBody } = existing;
343
+ if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
344
+ return createResult({
345
+ actionType,
346
+ status: ActionStatus.EXECUTE_FAILED,
347
+ error: 'existing codex manual-install requirement conflicts with the current frozen action; refusing to overwrite',
348
+ });
349
+ }
350
+ } else {
351
+ await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: new Date().toISOString() });
352
+ }
353
+
354
+ return createResult({
355
+ actionType,
356
+ status: ActionStatus.EXECUTED,
357
+ observation: {
358
+ installed: false,
359
+ manualInstallRequired: true,
360
+ platform: 'codex',
361
+ plugin: action.plugin,
362
+ version: action.version,
363
+ ref,
364
+ planDigest,
365
+ attestationDir,
366
+ instructions,
367
+ },
368
+ });
369
+ }