release-skill 0.9.23 → 0.9.24
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 +28 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +22 -30
- package/README.zh-CN.md +21 -26
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +839 -1029
- package/adapters/claude/skills/release-help/SKILL.md +2 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +839 -1029
- package/adapters/codex/skills/release-help/SKILL.md +2 -2
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +839 -1029
- package/adapters/cursor/skills/release-help/SKILL.md +2 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +839 -1029
- package/adapters/kimi/skills/release-help/SKILL.md +2 -2
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +839 -1029
- package/adapters/qoder/skills/release-help/SKILL.md +2 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +839 -1029
- package/adapters/workbuddy/skills/release-help/SKILL.md +2 -2
- package/bin/release-skill.bundle.mjs +839 -1029
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/05-evidence-and-errors.md +2 -2
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/sync-public-files.mjs +1 -1
- package/skills/release-help/SKILL.md +2 -2
- package/skills-src/release-help/SKILL.md +2 -2
- package/src/artifacts/producer-registry.mjs +4 -3
- package/src/commands/prepare.mjs +5 -30
- package/src/core/baseline.mjs +9 -1
- package/src/core/redact.mjs +12 -5
- package/src/core/source-authority.mjs +28 -3
- package/src/npm/npm-entry-closure.mjs +71 -9
- package/src/producers/build-adapters.mjs +2 -1
- package/src/producers/render-public-assets.mjs +2 -1
- package/src/producers/sync-skills.mjs +2 -1
- package/src/snapshot/frozen.mjs +19 -14
- package/src/core/run-retention.mjs +0 -227
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/** Best-effort cleanup of superseded, complete production run lineages. */
|
|
2
|
-
|
|
3
|
-
import { lstat, readFile, readdir, realpath, rm as fsRm } from 'node:fs/promises';
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { resolveContained } from 'skill-family-harness-node';
|
|
6
|
-
import { assertImmutablePlanAuthority, computePlanDigest, validatePlan } from './plan.mjs';
|
|
7
|
-
import { loadRun, validateRunCheckpointMapping, validateRunLineage, validateRunPlanDigest } from './run.mjs';
|
|
8
|
-
import { normalizePostPublishView, postPublishActionId } from './postpublish.mjs';
|
|
9
|
-
|
|
10
|
-
const NON_TERMINAL = new Set(['PUBLISHING', 'PARTIAL', 'NEEDS_INPUT', 'BLOCKED', 'DISTRIBUTING']);
|
|
11
|
-
const COMPLETED_CHECKPOINTS = new Set(['succeeded', 'skipped']);
|
|
12
|
-
|
|
13
|
-
function validDirectName(name) {
|
|
14
|
-
return typeof name === 'string' && name.length > 0 && name !== '.' && name !== '..'
|
|
15
|
-
&& !isAbsolute(name) && !name.includes('/') && !name.includes('\\');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function jsonFile(path) {
|
|
19
|
-
try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function physicalContained(root, declaredPath) {
|
|
23
|
-
if (typeof declaredPath !== 'string' || !isAbsolute(declaredPath)) return null;
|
|
24
|
-
const lexicalRoot = resolve(root);
|
|
25
|
-
const physicalRoot = await realpath(lexicalRoot);
|
|
26
|
-
const lexical = relative(lexicalRoot, resolve(declaredPath));
|
|
27
|
-
if (!lexical || isAbsolute(lexical) || lexical === '..' || lexical.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) return null;
|
|
28
|
-
try {
|
|
29
|
-
const contained = await resolveContained(lexicalRoot, lexical);
|
|
30
|
-
const target = await realpath(contained);
|
|
31
|
-
const targetRel = relative(physicalRoot, target);
|
|
32
|
-
if (!targetRel || isAbsolute(targetRel) || targetRel === '..' || targetRel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) return null;
|
|
33
|
-
return target;
|
|
34
|
-
} catch { return null; }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function loadPlanFromRun(releaseDir, run) {
|
|
38
|
-
if (!run?.planPath) return null;
|
|
39
|
-
const planPath = await physicalContained(releaseDir, run.planPath);
|
|
40
|
-
if (!planPath || basename(dirname(planPath)) !== 'plans') return null;
|
|
41
|
-
const plan = await jsonFile(planPath);
|
|
42
|
-
if (!plan) return null;
|
|
43
|
-
try {
|
|
44
|
-
validatePlan(plan);
|
|
45
|
-
assertImmutablePlanAuthority(planPath, plan);
|
|
46
|
-
const digest = computePlanDigest(plan);
|
|
47
|
-
if (run.planDigest !== digest || basename(planPath) !== `${digest}.json`) return null;
|
|
48
|
-
return { plan, planPath, digest };
|
|
49
|
-
} catch { return null; }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function postVerifyActions(plan) {
|
|
53
|
-
return normalizePostPublishView(plan).flatMap((declaration) => (
|
|
54
|
-
(declaration.hooks ?? []).filter((hook) => hook.phase === 'postVerify').map((hook) => ({
|
|
55
|
-
id: postPublishActionId({ planVersion: plan.planVersion, unitId: declaration.unitId, localId: hook.id }),
|
|
56
|
-
type: 'postpublish-hook',
|
|
57
|
-
}))
|
|
58
|
-
));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function postVerificationComplete(run) {
|
|
62
|
-
return run.checkpoints.every((checkpoint) => (
|
|
63
|
-
checkpoint.status === 'succeeded' || checkpoint.status === 'NO_CHANGE'
|
|
64
|
-
));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function publicationComplete(run) {
|
|
68
|
-
return run.checkpoints.every((cp) => COMPLETED_CHECKPOINTS.has(cp.status)
|
|
69
|
-
|| (['failed', 'deferred'].includes(cp.status)
|
|
70
|
-
&& ['claude-marketplace-install', 'codex-marketplace-install', 'kimi-marketplace-install', 'codebuddy-marketplace-install', 'qoder-marketplace-install'].includes(cp.actionType)));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function lineagePathsContained(run, runsRoot) {
|
|
74
|
-
let cursor = run;
|
|
75
|
-
for (let depth = 0; depth < 32; depth += 1) {
|
|
76
|
-
if (!cursor.sourceRunPath) return true;
|
|
77
|
-
const sourcePath = await physicalContained(runsRoot, cursor.sourceRunPath);
|
|
78
|
-
if (!sourcePath) return false;
|
|
79
|
-
cursor = await loadRun(sourcePath, { requireDigest: true });
|
|
80
|
-
}
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function validatePublicationCheckpoints(run, plan, runsRoot) {
|
|
85
|
-
let cursor = run;
|
|
86
|
-
for (let depth = 0; depth < 32; depth += 1) {
|
|
87
|
-
if (cursor.command === 'publish') {
|
|
88
|
-
validateRunCheckpointMapping(cursor, plan.externalActions ?? []);
|
|
89
|
-
return cursor.status === 'PUBLISHED' ? publicationComplete(cursor) : true;
|
|
90
|
-
}
|
|
91
|
-
if (!cursor.sourceRunPath) return false;
|
|
92
|
-
const sourcePath = await physicalContained(runsRoot, cursor.sourceRunPath);
|
|
93
|
-
if (!sourcePath) return false;
|
|
94
|
-
cursor = await loadRun(sourcePath, { requireDigest: true });
|
|
95
|
-
if (cursor.command === 'reconcile') validateRunCheckpointMapping(cursor, plan.externalActions ?? []);
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function validateTerminal({ candidate, planInfo, runsRoot }) {
|
|
101
|
-
const { run, runPath } = candidate;
|
|
102
|
-
if (!run || !planInfo) return false;
|
|
103
|
-
const { plan, planPath } = planInfo;
|
|
104
|
-
try {
|
|
105
|
-
if (!(await lineagePathsContained(run, runsRoot))) return false;
|
|
106
|
-
await validateRunLineage(run, { plan, planPath, runPath, production: Boolean(plan.production) });
|
|
107
|
-
validateRunPlanDigest(run, plan, { planPath });
|
|
108
|
-
if (run.command === 'verify' && run.status === 'VERIFIED') {
|
|
109
|
-
return validatePublicationCheckpoints(run, plan, runsRoot);
|
|
110
|
-
}
|
|
111
|
-
if (run.command !== 'postverify' || run.status !== 'DISTRIBUTED') return false;
|
|
112
|
-
validateRunCheckpointMapping(run, postVerifyActions(plan));
|
|
113
|
-
return postVerificationComplete(run)
|
|
114
|
-
&& validatePublicationCheckpoints(run, plan, runsRoot);
|
|
115
|
-
} catch { return false; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function scanCandidate(runsRoot, name) {
|
|
119
|
-
const dir = join(runsRoot, name);
|
|
120
|
-
const summary = await jsonFile(join(dir, 'summary.json'));
|
|
121
|
-
const runPath = join(dir, 'release-run.json');
|
|
122
|
-
let run = null;
|
|
123
|
-
let runExists = false;
|
|
124
|
-
let runCorrupt = false;
|
|
125
|
-
try {
|
|
126
|
-
const stat = await lstat(runPath);
|
|
127
|
-
runExists = true;
|
|
128
|
-
if (stat.isSymbolicLink() || !stat.isFile()) runCorrupt = true;
|
|
129
|
-
else run = await loadRun(runPath, { requireDigest: true });
|
|
130
|
-
} catch { if (runExists) runCorrupt = true; }
|
|
131
|
-
const statesExists = await lstat(join(dir, 'states')).then(() => true).catch(() => false);
|
|
132
|
-
let sealedFailedPrepare = false;
|
|
133
|
-
if (summary?.status === 'FAILED' && !runExists) {
|
|
134
|
-
try {
|
|
135
|
-
const evidence = await readFile(join(dir, 'evidence.jsonl'), 'utf8');
|
|
136
|
-
sealedFailedPrepare = evidence.split('\n').some((line) => JSON.parse(line)?.command === 'prepare');
|
|
137
|
-
} catch { sealedFailedPrepare = false; }
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
dir, name, summary, run, runPath, runExists, runCorrupt, statesExists, sealedFailedPrepare,
|
|
141
|
-
planDigest: run?.planDigest ?? summary?.planDigest ?? null,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function deleteCandidate(runsRoot, candidate, rmFn) {
|
|
146
|
-
const contained = await physicalContained(runsRoot, join(runsRoot, candidate.name));
|
|
147
|
-
const physicalRoot = await realpath(runsRoot);
|
|
148
|
-
if (!contained || dirname(contained) !== physicalRoot) throw new Error('run path containment failed');
|
|
149
|
-
const stat = await lstat(contained);
|
|
150
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('run candidate is not a real directory');
|
|
151
|
-
await rmFn(contained, { recursive: true, force: false });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** @returns {Promise<{scanned:number,deleted:number,errors:number,diagnostics:Array}>} */
|
|
155
|
-
export async function cleanupRunRetention({ releaseDir, currentRunDir, rmFn = fsRm } = {}) {
|
|
156
|
-
const result = { scanned: 0, deleted: 0, errors: 0, diagnostics: [] };
|
|
157
|
-
if (!releaseDir) return result;
|
|
158
|
-
const runsRoot = resolve(releaseDir, 'runs');
|
|
159
|
-
let rootStat;
|
|
160
|
-
try { rootStat = await lstat(runsRoot); } catch { return result; }
|
|
161
|
-
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return result;
|
|
162
|
-
const physicalRunsRoot = await realpath(runsRoot).catch(() => null);
|
|
163
|
-
if (!physicalRunsRoot) return result;
|
|
164
|
-
let entries;
|
|
165
|
-
try { entries = await readdir(physicalRunsRoot, { withFileTypes: true }); } catch { return result; }
|
|
166
|
-
const candidates = [];
|
|
167
|
-
for (const entry of entries) {
|
|
168
|
-
result.scanned += 1;
|
|
169
|
-
if (!entry.isDirectory() || entry.isSymbolicLink() || !validDirectName(entry.name)) continue;
|
|
170
|
-
candidates.push(await scanCandidate(physicalRunsRoot, entry.name));
|
|
171
|
-
}
|
|
172
|
-
const protectedNames = new Set();
|
|
173
|
-
if (currentRunDir) protectedNames.add(basename(resolve(currentRunDir)));
|
|
174
|
-
const currentPlan = await jsonFile(join(releaseDir, 'release-plan.json'));
|
|
175
|
-
let currentPlanDigest = null;
|
|
176
|
-
try { if (currentPlan) currentPlanDigest = computePlanDigest(currentPlan); } catch { currentPlanDigest = null; }
|
|
177
|
-
const groups = new Map();
|
|
178
|
-
for (const candidate of candidates) {
|
|
179
|
-
if (!candidate.planDigest) continue;
|
|
180
|
-
if (!groups.has(candidate.planDigest)) groups.set(candidate.planDigest, []);
|
|
181
|
-
groups.get(candidate.planDigest).push(candidate);
|
|
182
|
-
if (currentPlanDigest && candidate.planDigest === currentPlanDigest) protectedNames.add(candidate.name);
|
|
183
|
-
}
|
|
184
|
-
const completeGroups = [];
|
|
185
|
-
for (const [planDigest, group] of groups) {
|
|
186
|
-
const authority = group.find((item) => item.run?.planDigest === planDigest);
|
|
187
|
-
const planInfo = authority ? await loadPlanFromRun(releaseDir, authority.run) : null;
|
|
188
|
-
const requiresPostVerify = Boolean(planInfo && normalizePostPublishView(planInfo.plan)
|
|
189
|
-
.some((declaration) => (declaration.hooks ?? []).some((hook) => hook.phase === 'postVerify')));
|
|
190
|
-
const terminals = group.filter((item) => requiresPostVerify
|
|
191
|
-
? item.run?.status === 'DISTRIBUTED'
|
|
192
|
-
: item.run?.status === 'VERIFIED');
|
|
193
|
-
const terminalResults = planInfo
|
|
194
|
-
? await Promise.all(terminals.map((item) => validateTerminal({ candidate: item, planInfo, runsRoot })))
|
|
195
|
-
: [];
|
|
196
|
-
const terminalValid = terminalResults.some(Boolean);
|
|
197
|
-
const unsafe = group.some((item) => item.runCorrupt
|
|
198
|
-
|| (!item.run && item.summary?.status !== 'PREPARED')
|
|
199
|
-
|| (item.run && NON_TERMINAL.has(item.run.status))
|
|
200
|
-
|| (item.run?.status === 'PUBLISHED' && !publicationComplete(item.run))
|
|
201
|
-
|| (item.run && ((requiresPostVerify && item.run.status === 'DISTRIBUTED')
|
|
202
|
-
|| (!requiresPostVerify && item.run.status === 'VERIFIED'))
|
|
203
|
-
&& !terminalResults[terminals.indexOf(item)]));
|
|
204
|
-
if (!terminalValid || unsafe) {
|
|
205
|
-
for (const item of group) protectedNames.add(item.name);
|
|
206
|
-
} else {
|
|
207
|
-
completeGroups.push({ group, createdAt: planInfo.plan.createdAt ?? '', planDigest });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
completeGroups.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)) || b.planDigest.localeCompare(a.planDigest));
|
|
211
|
-
for (const old of completeGroups.slice(1)) {
|
|
212
|
-
for (const item of old.group) {
|
|
213
|
-
if (protectedNames.has(item.name)) continue;
|
|
214
|
-
try { await deleteCandidate(physicalRunsRoot, item, rmFn); result.deleted += 1; }
|
|
215
|
-
catch (error) { result.errors += 1; result.diagnostics.push({ name: item.name, code: error?.code ?? 'RUN_RETENTION_DELETE_FAILED' }); }
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
for (const candidate of candidates) {
|
|
219
|
-
const hasPlanTrace = Boolean(candidate.planDigest || candidate.summary?.planPath || candidate.summary?.externalActions || candidate.summary?.checkpoints);
|
|
220
|
-
const isFailedPrepare = candidate.name.startsWith('prepare-') && candidate.summary?.status === 'FAILED'
|
|
221
|
-
&& candidate.sealedFailedPrepare && !candidate.runExists && !candidate.statesExists && !hasPlanTrace;
|
|
222
|
-
if (!isFailedPrepare || protectedNames.has(candidate.name)) continue;
|
|
223
|
-
try { await deleteCandidate(physicalRunsRoot, candidate, rmFn); result.deleted += 1; }
|
|
224
|
-
catch (error) { result.errors += 1; result.diagnostics.push({ name: candidate.name, code: error?.code ?? 'RUN_RETENTION_DELETE_FAILED' }); }
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
}
|