release-skill 0.1.10 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +33 -0
  7. package/INSTALL.md +24 -4
  8. package/INSTALL.zh-CN.md +22 -4
  9. package/README.md +19 -32
  10. package/README.zh-CN.md +14 -25
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +2721 -1843
  14. package/adapters/claude/schemas/.render-manifest.json +8 -8
  15. package/adapters/claude/schemas/approval-record.schema.json +1 -1
  16. package/adapters/claude/schemas/release-plan.schema.json +6 -2
  17. package/adapters/claude/schemas/release-project.schema.json +14 -0
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +2721 -1843
  20. package/adapters/codex/schemas/.render-manifest.json +8 -8
  21. package/adapters/codex/schemas/approval-record.schema.json +1 -1
  22. package/adapters/codex/schemas/release-plan.schema.json +6 -2
  23. package/adapters/codex/schemas/release-project.schema.json +14 -0
  24. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  25. package/adapters/kimi/bin/release-skill.bundle.mjs +2721 -1843
  26. package/adapters/kimi/schemas/.render-manifest.json +8 -8
  27. package/adapters/kimi/schemas/approval-record.schema.json +1 -1
  28. package/adapters/kimi/schemas/release-plan.schema.json +6 -2
  29. package/adapters/kimi/schemas/release-project.schema.json +14 -0
  30. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +10 -0
  31. package/adapters/workbuddy/bin/release-skill.bundle.mjs +85363 -0
  32. package/adapters/workbuddy/bin/release-skill.mjs +54 -0
  33. package/adapters/workbuddy/native/safe-write/binding.gyp +41 -0
  34. package/adapters/workbuddy/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  35. package/adapters/workbuddy/native/safe-write/prebuilds.json +24 -0
  36. package/adapters/workbuddy/native/safe-write/src/safe_write.cc +2032 -0
  37. package/adapters/workbuddy/schemas/.render-manifest.json +37 -0
  38. package/adapters/workbuddy/schemas/approval-record.schema.json +115 -0
  39. package/adapters/workbuddy/schemas/artifact-lock.schema.json +111 -0
  40. package/adapters/workbuddy/schemas/artifact-plan.schema.json +52 -0
  41. package/adapters/workbuddy/schemas/artifact-policy.schema.json +76 -0
  42. package/adapters/workbuddy/schemas/evidence-event.schema.json +89 -0
  43. package/adapters/workbuddy/schemas/release-plan.schema.json +882 -0
  44. package/adapters/workbuddy/schemas/release-project.schema.json +909 -0
  45. package/adapters/workbuddy/schemas/release-run.schema.json +343 -0
  46. package/adapters/workbuddy/skills/release-assess/SKILL.md +51 -0
  47. package/adapters/workbuddy/skills/release-help/SKILL.md +77 -0
  48. package/adapters/workbuddy/skills/release-prepare/SKILL.md +92 -0
  49. package/adapters/workbuddy/skills/release-publish/SKILL.md +57 -0
  50. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +73 -0
  51. package/adapters/workbuddy/skills/release-setup/SKILL.md +95 -0
  52. package/adapters/workbuddy/skills/release-verify/SKILL.md +70 -0
  53. package/bin/release-skill-cli.mjs +3 -0
  54. package/bin/release-skill.bundle.mjs +2721 -1843
  55. package/package.json +9 -2
  56. package/references/.render-manifest.json +8 -8
  57. package/references/01-state-machine.md +5 -5
  58. package/references/02-project-config.md +1 -1
  59. package/references/05-evidence-and-errors.md +1 -1
  60. package/references/06-adapter-contract.md +41 -1
  61. package/schemas/.render-manifest.json +8 -8
  62. package/schemas/approval-record.schema.json +1 -1
  63. package/schemas/release-plan.schema.json +6 -2
  64. package/schemas/release-project.schema.json +14 -0
  65. package/scripts/sync-public-files.mjs +481 -0
  66. package/src/adapters/contract.mjs +60 -0
  67. package/src/adapters/plugin-marketplace.mjs +289 -736
  68. package/src/commands/prepare.mjs +195 -182
  69. package/src/commands/publish.mjs +438 -122
  70. package/src/commands/reconcile.mjs +369 -191
  71. package/src/commands/verify.mjs +13 -2
  72. package/src/core/approval.mjs +72 -45
  73. package/src/core/baseline.mjs +5 -0
  74. package/src/core/checkpoints.mjs +143 -0
  75. package/src/core/evidence.mjs +30 -3
  76. package/src/core/hook-cache.mjs +254 -0
  77. package/src/core/hooks.mjs +37 -1
  78. package/src/core/observe-retry.mjs +223 -0
  79. package/src/core/plan.mjs +162 -253
  80. package/src/platforms/kimi.mjs +514 -0
  81. package/src/platforms/registry.mjs +393 -0
  82. package/src/producers/build-adapters.mjs +49 -23
@@ -54,7 +54,7 @@ function validateHook(hook) {
54
54
  throw new ReleaseError('INVALID_HOOK', 'hook must be a non-null object');
55
55
  }
56
56
 
57
- const { command, cwd, timeoutMs, envAllowlist } = hook;
57
+ const { command, cwd, timeoutMs, envAllowlist, cacheable, cacheInputs } = hook;
58
58
 
59
59
  // command: required, non-empty array of strings
60
60
  if (!Array.isArray(command) || command.length === 0) {
@@ -92,6 +92,40 @@ function validateHook(hook) {
92
92
  }
93
93
  }
94
94
  }
95
+
96
+ // cacheable: optional boolean opting the hook into the incremental result
97
+ // cache (T3.2). Absence means the hook always runs in full (default).
98
+ if (cacheable !== undefined && typeof cacheable !== 'boolean') {
99
+ throw new ReleaseError('INVALID_HOOK', 'hook.cacheable must be a boolean when provided');
100
+ }
101
+
102
+ // cacheInputs: optional non-empty array of non-empty glob strings declaring
103
+ // the hook's full input set (used to fingerprint the cache key).
104
+ if (cacheInputs !== undefined) {
105
+ if (!Array.isArray(cacheInputs) || cacheInputs.length === 0) {
106
+ throw new ReleaseError(
107
+ 'INVALID_HOOK',
108
+ 'hook.cacheInputs must be a non-empty array of glob strings when provided',
109
+ );
110
+ }
111
+ for (const glob of cacheInputs) {
112
+ if (typeof glob !== 'string' || glob.length === 0) {
113
+ throw new ReleaseError(
114
+ 'INVALID_HOOK',
115
+ 'every element of hook.cacheInputs must be a non-empty string',
116
+ );
117
+ }
118
+ }
119
+ }
120
+
121
+ // A cacheable hook MUST declare its inputs: a cache without an input
122
+ // declaration is untrustworthy (any unseen change could cause a false hit).
123
+ if (cacheable === true && (!Array.isArray(cacheInputs) || cacheInputs.length === 0)) {
124
+ throw new ReleaseError(
125
+ 'INVALID_HOOK',
126
+ 'hook.cacheable=true requires a non-empty hook.cacheInputs',
127
+ );
128
+ }
95
129
  }
96
130
 
97
131
  // ---------------------------------------------------------------------------
@@ -146,6 +180,8 @@ function buildFilteredEnv(envAllowlist, contextEnv) {
146
180
  * @param {string} [hook.cwd] - Relative (to root) working directory.
147
181
  * @param {number} [hook.timeoutMs] - Kill child after this many ms.
148
182
  * @param {string[]} [hook.envAllowlist] - Extra env keys to pass through.
183
+ * @param {boolean} [hook.cacheable] - Opt into the incremental result cache.
184
+ * @param {string[]} [hook.cacheInputs] - Input globs fingerprinting the cache key.
149
185
  *
150
186
  * @param {Object} context
151
187
  * @param {string} context.root - Absolute project root.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Bounded observe-with-retry for transient (PROPAGATING) remote states.
3
+ *
4
+ * After an `execute` writes to a remote system, the write may not be
5
+ * immediately observable: package registries have eventual consistency
6
+ * (e.g. `npm publish` succeeds but `npm view` cannot find the new
7
+ * version for tens of seconds), and transient network errors happen.
8
+ *
9
+ * This module retries ONLY read-only `observe` calls while the remote
10
+ * state is "information-insufficient" (missing / empty / threw). The
11
+ * moment a concrete observation is read back, the caller classifies it
12
+ * (CONSISTENT / CONFLICTING / TERMINAL_MISSING). A present-but-
13
+ * mismatched observation (CONFLICTING) is NEVER retried: a conflict
14
+ * is a real, authoritative disagreement that must fail closed and be
15
+ * resolved by a human. This is exactly the safety semantics required by
16
+ * the release-skill governance (fail-closed, observe stays read-only,
17
+ * no execute retry, no auto-overwrite of remote state).
18
+ *
19
+ * Callers wire this into the four observe call sites (publish executeCheckpoint
20
+ * and the three reconcile observation points). The retry policy is fixed and
21
+ * not user-configurable on purpose: it never enters the frozen plan or the
22
+ * approval record, so changing it cannot invalidate an approved release.
23
+ *
24
+ * @module core/observe-retry
25
+ */
26
+
27
+ /**
28
+ * Default retry policy.
29
+ *
30
+ * The first observe call happens immediately; each subsequent retry waits
31
+ * the corresponding delay. With `maxAttempts: 5` and four delays the
32
+ * total worst-case retry window is ~150s (10+20+40+80). That is the
33
+ * cost of avoiding a manual reconcile loop (minutes of human time).
34
+ *
35
+ * @type {{ maxAttempts: number, delaysMs: ReadonlyArray<number> }}
36
+ */
37
+ export const DEFAULT_OBSERVE_RETRY_POLICY = Object.freeze({
38
+ maxAttempts: 5,
39
+ delaysMs: Object.freeze([10_000, 20_000, 40_000, 80_000]),
40
+ });
41
+
42
+ /**
43
+ * Reduce a retry policy so its total delay window fits inside a hard
44
+ * timeout (e.g. a marketplace action's `timeoutMs`, valid range 30s-900s).
45
+ *
46
+ * If the policy already fits, it is returned unchanged. Otherwise delays are
47
+ * dropped from the end (longest first) until the remaining sum fits, and
48
+ * `maxAttempts` is recomputed as `delays.length + 1` (one immediate
49
+ * observe plus one per remaining delay). Always leaves at least one attempt.
50
+ *
51
+ * @param {{ maxAttempts: number, delaysMs: ReadonlyArray<number> }} policy
52
+ * @param {number|null|undefined} timeoutMs - Hard ceiling in milliseconds.
53
+ * @returns {{ maxAttempts: number, delaysMs: number[] }}
54
+ */
55
+ export function clampPolicyToTimeout(policy, timeoutMs) {
56
+ if (timeoutMs == null || typeof timeoutMs !== 'number' || timeoutMs <= 0) {
57
+ return policy;
58
+ }
59
+ const delays = [...policy.delaysMs];
60
+ let total = delays.reduce((sum, ms) => sum + ms, 0);
61
+ // Already fits the timeout: return the SAME policy object, unchanged.
62
+ if (total <= timeoutMs) {
63
+ return policy;
64
+ }
65
+ while (delays.length > 0 && total > timeoutMs) {
66
+ const dropped = delays.pop();
67
+ const newTotal = total - dropped;
68
+ if (newTotal <= timeoutMs) {
69
+ total = newTotal;
70
+ break;
71
+ }
72
+ total = newTotal;
73
+ }
74
+ const maxAttempts = Math.max(1, delays.length + 1);
75
+ return { maxAttempts, delaysMs: delays };
76
+ }
77
+
78
+ function defaultClock() {
79
+ return new Date().toISOString();
80
+ }
81
+
82
+ /**
83
+ * Default inter-attempt delay.
84
+ *
85
+ * Test escape hatch: when `RELEASE_SKILL_OBSERVE_RETRY_NO_WAIT=1` is set,
86
+ * the delay resolves immediately. This ONLY skips the wall-clock wait for
87
+ * spawned-CLI test sandboxes (in-process tests inject a sleep instead);
88
+ * attempt counts, ordering, and PROPAGATING/CONFLICTING classification are
89
+ * unchanged, so it cannot weaken any fail-closed decision.
90
+ */
91
+ function defaultSleep(ms) {
92
+ if (process.env.RELEASE_SKILL_OBSERVE_RETRY_NO_WAIT === '1') {
93
+ return Promise.resolve();
94
+ }
95
+ return new Promise((resolve) => setTimeout(resolve, ms));
96
+ }
97
+
98
+ /**
99
+ * Decide whether an observe/verify result is "information-insufficient"
100
+ * (PROPAGATING) and therefore a candidate for retry.
101
+ *
102
+ * Returns `true` when the remote state is unknown or explicitly absent:
103
+ * - the call threw (no result at all)
104
+ * - the observation is missing or an empty object
105
+ * - an explicit absence marker is present (`exists:false`, `published:false`,
106
+ * `installed:false`, empty commit strings)
107
+ *
108
+ * Returns `false` when a concrete observation was read back. A present
109
+ * observation — even if it does NOT match the expected state — is a
110
+ * CONFLICTING signal, not a propagation delay, and must NOT be retried.
111
+ *
112
+ * @param {{ observation?: Object|null, error?: string|null }|null} result
113
+ * @returns {boolean}
114
+ */
115
+ export function isPropagatingMissing(result) {
116
+ if (result == null) return true;
117
+ const { observation } = result;
118
+ if (observation == null) return true;
119
+ if (Object.keys(observation).length === 0) return true;
120
+ if (observation.exists === false) return true;
121
+ if (observation.remoteCommit === '') return true;
122
+ if (observation.commit === '') return true;
123
+ if (observation.published === false) return true;
124
+ if (observation.installed === false) return true;
125
+ // NOTE: a thrown observe surfaces as `error` with a null-ish observation,
126
+ // which is already covered by the `observation == null` / empty checks above.
127
+ return false;
128
+ }
129
+
130
+ /**
131
+ * Retry a read-only `observe` (or `verify`, which is observe+match) while
132
+ * the remote state is information-insufficient.
133
+ *
134
+ * @param {Object} options
135
+ * @param {Function} options.observe - `(action, context) => Promise<result>`.
136
+ * The result must expose `.observation` and `.error` (the `createResult`
137
+ * shape). Throwing is treated as a propagating miss and retried.
138
+ * @param {Object} options.action - Adapter action passed through to `observe`.
139
+ * @param {Object} options.context - Adapter context passed through to `observe`.
140
+ * @param {Function} [options.isMissing] - Override for the missing check
141
+ * (defaults to {@link isPropagatingMissing}).
142
+ * @param {Object} [options.policy] - Retry policy (defaults to
143
+ * {@link DEFAULT_OBSERVE_RETRY_POLICY}).
144
+ * @param {Function} [options.clock] - `() => string` timestamp for evidence.
145
+ * @param {Function} [options.sleep] - `(ms) => Promise` delay (injected in tests).
146
+ * @param {Function} [options.onAttempt] - `async (info) => void` evidence hook,
147
+ * called once per attempt with `{ attempt, maxAttempts, missing, delayMs,
148
+ * threw, observation, error, timestamp }`.
149
+ * @returns {Promise<{ result: Object|null, missing: boolean, attempts: number, exhausted: boolean, threw: boolean }>}
150
+ * `result` is the last raw observe/verify result (or a normalized
151
+ * `{ observation: null, error }` when the final attempt threw). It is
152
+ * handed back to the caller so the caller can run its own
153
+ * CONSISTENT/CONFLICTING/TERMINAL_MISSING classification unchanged.
154
+ */
155
+ export async function observeWithRetry({
156
+ observe,
157
+ action,
158
+ context,
159
+ isMissing = isPropagatingMissing,
160
+ policy = DEFAULT_OBSERVE_RETRY_POLICY,
161
+ clock = defaultClock,
162
+ sleep = defaultSleep,
163
+ onAttempt,
164
+ } = {}) {
165
+ const delays = [...(policy.delaysMs ?? [])];
166
+ const maxAttempts = policy.maxAttempts ?? delays.length + 1;
167
+
168
+ let lastResult = null;
169
+ let lastThrew = false;
170
+
171
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
172
+ let result;
173
+ let threw = false;
174
+ try {
175
+ result = await observe(action, context);
176
+ } catch (error) {
177
+ threw = true;
178
+ lastThrew = true;
179
+ result = { observation: null, error: error?.message ?? String(error) };
180
+ }
181
+ lastResult = result;
182
+
183
+ const missing = isMissing(result);
184
+ // The delay that will actually be applied AFTER this attempt: only when
185
+ // the state is still missing and a delay remains. On the final attempt
186
+ // (or a resolved attempt) this is 0, reported truthfully.
187
+ const willSleep = missing && attempt < delays.length;
188
+ const delayMs = willSleep ? delays[attempt] : 0;
189
+
190
+ if (onAttempt) {
191
+ await onAttempt({
192
+ attempt: attempt + 1,
193
+ maxAttempts,
194
+ missing,
195
+ delayMs,
196
+ threw,
197
+ observation: result?.observation ?? null,
198
+ error: result?.error ?? null,
199
+ timestamp: clock(),
200
+ });
201
+ }
202
+
203
+ // A concrete observation was read back: not a propagation delay.
204
+ // Return immediately so the caller can classify it (including a real
205
+ // CONFLICTING mismatch, which must never be retried).
206
+ if (!missing) {
207
+ return { result, missing: false, attempts: attempt + 1, exhausted: false, threw: false };
208
+ }
209
+
210
+ // Still missing: wait before the next attempt (unless this was the last).
211
+ if (willSleep) {
212
+ await sleep(delays[attempt]);
213
+ }
214
+ }
215
+
216
+ return {
217
+ result: lastResult,
218
+ missing: true,
219
+ attempts: maxAttempts,
220
+ exhausted: true,
221
+ threw: lastThrew,
222
+ };
223
+ }