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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +33 -0
- package/INSTALL.md +24 -4
- package/INSTALL.zh-CN.md +22 -4
- package/README.md +19 -32
- package/README.zh-CN.md +14 -25
- 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 +2721 -1843
- package/adapters/claude/schemas/.render-manifest.json +8 -8
- package/adapters/claude/schemas/approval-record.schema.json +1 -1
- package/adapters/claude/schemas/release-plan.schema.json +6 -2
- package/adapters/claude/schemas/release-project.schema.json +14 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/codex/schemas/.render-manifest.json +8 -8
- package/adapters/codex/schemas/approval-record.schema.json +1 -1
- package/adapters/codex/schemas/release-plan.schema.json +6 -2
- package/adapters/codex/schemas/release-project.schema.json +14 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/kimi/schemas/.render-manifest.json +8 -8
- package/adapters/kimi/schemas/approval-record.schema.json +1 -1
- package/adapters/kimi/schemas/release-plan.schema.json +6 -2
- package/adapters/kimi/schemas/release-project.schema.json +14 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +10 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +85363 -0
- package/adapters/workbuddy/bin/release-skill.mjs +54 -0
- package/adapters/workbuddy/native/safe-write/binding.gyp +41 -0
- package/adapters/workbuddy/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/workbuddy/native/safe-write/prebuilds.json +24 -0
- package/adapters/workbuddy/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/workbuddy/schemas/.render-manifest.json +37 -0
- package/adapters/workbuddy/schemas/approval-record.schema.json +115 -0
- package/adapters/workbuddy/schemas/artifact-lock.schema.json +111 -0
- package/adapters/workbuddy/schemas/artifact-plan.schema.json +52 -0
- package/adapters/workbuddy/schemas/artifact-policy.schema.json +76 -0
- package/adapters/workbuddy/schemas/evidence-event.schema.json +89 -0
- package/adapters/workbuddy/schemas/release-plan.schema.json +882 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +909 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +343 -0
- package/adapters/workbuddy/skills/release-assess/SKILL.md +51 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +77 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +92 -0
- package/adapters/workbuddy/skills/release-publish/SKILL.md +57 -0
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +73 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +95 -0
- package/adapters/workbuddy/skills/release-verify/SKILL.md +70 -0
- package/bin/release-skill-cli.mjs +3 -0
- package/bin/release-skill.bundle.mjs +2721 -1843
- package/package.json +9 -2
- package/references/.render-manifest.json +8 -8
- package/references/01-state-machine.md +5 -5
- package/references/02-project-config.md +1 -1
- package/references/05-evidence-and-errors.md +1 -1
- package/references/06-adapter-contract.md +41 -1
- package/schemas/.render-manifest.json +8 -8
- package/schemas/approval-record.schema.json +1 -1
- package/schemas/release-plan.schema.json +6 -2
- package/schemas/release-project.schema.json +14 -0
- package/scripts/sync-public-files.mjs +481 -0
- package/src/adapters/contract.mjs +60 -0
- package/src/adapters/plugin-marketplace.mjs +289 -736
- package/src/commands/prepare.mjs +195 -182
- package/src/commands/publish.mjs +438 -122
- package/src/commands/reconcile.mjs +369 -191
- package/src/commands/verify.mjs +13 -2
- package/src/core/approval.mjs +72 -45
- package/src/core/baseline.mjs +5 -0
- package/src/core/checkpoints.mjs +143 -0
- package/src/core/evidence.mjs +30 -3
- package/src/core/hook-cache.mjs +254 -0
- package/src/core/hooks.mjs +37 -1
- package/src/core/observe-retry.mjs +223 -0
- package/src/core/plan.mjs +162 -253
- package/src/platforms/kimi.mjs +514 -0
- package/src/platforms/registry.mjs +393 -0
- package/src/producers/build-adapters.mjs +49 -23
package/src/core/hooks.mjs
CHANGED
|
@@ -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
|
+
}
|