release-skill 0.9.37 → 0.9.38
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 +32 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +34 -14
- package/README.zh-CN.md +37 -14
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1912 -1446
- package/adapters/claude/schemas/release-plan.schema.json +1 -0
- package/adapters/claude/schemas/test-intake.schema.json +1 -0
- package/adapters/claude/skills/release-help/SKILL.md +36 -41
- package/adapters/claude/skills/release-prepare/SKILL.md +21 -1
- package/adapters/claude/skills/release-publish/SKILL.md +11 -1
- package/adapters/claude/skills/release-reconcile/SKILL.md +14 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1912 -1446
- package/adapters/codex/schemas/release-plan.schema.json +1 -0
- package/adapters/codex/schemas/test-intake.schema.json +1 -0
- package/adapters/codex/skills/release-help/SKILL.md +36 -41
- package/adapters/codex/skills/release-prepare/SKILL.md +21 -1
- package/adapters/codex/skills/release-publish/SKILL.md +11 -1
- package/adapters/codex/skills/release-reconcile/SKILL.md +14 -0
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +1912 -1446
- package/adapters/cursor/schemas/release-plan.schema.json +1 -0
- package/adapters/cursor/schemas/test-intake.schema.json +1 -0
- package/adapters/cursor/skills/release-help/SKILL.md +36 -41
- package/adapters/cursor/skills/release-prepare/SKILL.md +21 -1
- package/adapters/cursor/skills/release-publish/SKILL.md +11 -1
- package/adapters/cursor/skills/release-reconcile/SKILL.md +14 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1912 -1446
- package/adapters/kimi/schemas/release-plan.schema.json +1 -0
- package/adapters/kimi/schemas/test-intake.schema.json +1 -0
- package/adapters/kimi/skills/release-help/SKILL.md +36 -41
- package/adapters/kimi/skills/release-prepare/SKILL.md +21 -1
- package/adapters/kimi/skills/release-publish/SKILL.md +11 -1
- package/adapters/kimi/skills/release-reconcile/SKILL.md +14 -0
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +1912 -1446
- package/adapters/qoder/schemas/release-plan.schema.json +1 -0
- package/adapters/qoder/schemas/test-intake.schema.json +1 -0
- package/adapters/qoder/skills/release-help/SKILL.md +36 -41
- package/adapters/qoder/skills/release-prepare/SKILL.md +21 -1
- package/adapters/qoder/skills/release-publish/SKILL.md +11 -1
- package/adapters/qoder/skills/release-reconcile/SKILL.md +14 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1912 -1446
- package/adapters/workbuddy/schemas/release-plan.schema.json +1 -0
- package/adapters/workbuddy/schemas/test-intake.schema.json +1 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +36 -41
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +21 -1
- package/adapters/workbuddy/skills/release-publish/SKILL.md +11 -1
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +14 -0
- package/bin/release-skill-cli.mjs +5 -0
- package/bin/release-skill.bundle.mjs +1912 -1446
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/01-state-machine.md +26 -0
- package/references/02-project-config.md +20 -0
- package/schemas/release-plan.schema.json +1 -0
- package/schemas/test-intake.schema.json +1 -0
- package/skills/release-help/SKILL.md +36 -41
- package/skills/release-prepare/SKILL.md +21 -1
- package/skills/release-publish/SKILL.md +11 -1
- package/skills/release-reconcile/SKILL.md +14 -0
- package/skills-src/release-help/SKILL.md +36 -41
- package/skills-src/release-prepare/SKILL.md +21 -1
- package/skills-src/release-publish/SKILL.md +11 -1
- package/skills-src/release-reconcile/SKILL.md +14 -0
- package/src/commands/approve.mjs +33 -24
- package/src/commands/prepare.mjs +9 -2
- package/src/commands/publish.mjs +10 -2
- package/src/commands/reconcile.mjs +2 -0
- package/src/commands/route.mjs +2 -159
- package/src/core/plan-replacement.mjs +371 -0
- package/src/core/pre-authority-failure.mjs +241 -0
- package/src/core/test-candidate.mjs +15 -67
|
@@ -77,3 +77,17 @@ PUBLISHED 后立即用新 `runPath` 运行 verify。VERIFIED 后发布完成。P
|
|
|
77
77
|
纯问询或转交时短答:写明实际回答、适用范围或理由、必要限制和下一步。建议保持为建议。
|
|
78
78
|
|
|
79
79
|
执行路径说明本轮实际阶段、范围、退出码和结构化结果,引用源 run、计划和命令返回的新 `runPath`。保留 `PARTIAL`、`BLOCKED`、远端冲突和人工后续。仅当退出码为 0 且状态为 `PUBLISHED` 时,把恢复写为外写检查点已齐;随后用新 `runPath` 进入 verify。`PUBLISHED` 与 `VERIFIED` 按各自既有通过条件分别报告。已有 run 文件按原合同呈现。栏目只收录当前命令已经返回的事实。
|
|
80
|
+
|
|
81
|
+
## 0.9.38:替代未发布计划
|
|
82
|
+
|
|
83
|
+
本功能适用于同一项目、相同发布单元ID集合、目标版本及外部目标下,旧正式计划处于 `PREPARED` 状态且未产生任何发布运行权威的场景。仅在生产环境的 `testIntake` 入口启用,用于显式替代尚未发布的正式计划。已有首个运行状态,或处于 `PARTIAL`、`PUBLISHED`、`VERIFIED` 的计划,不属于这一例外。替代不代表发布成功或独立验收。
|
|
84
|
+
|
|
85
|
+
发布零外写诊断须独立于目标归属判断。仅当已核实 producer 版本为 0.9.3、0.9.14、0.9.17、0.9.33、0.9.37 或本版 0.9.38 时,且满足单一版本、完整连续事件、相符封口、完整必要前置门禁顺序,同时无 `release-run.json`、任何 `state` 权威、执行或检查点活动,方可作为零外写诊断依据。三类失败情形在满足上述完整条件时仍可作为诊断:来源门禁失败;0.9.17 在只读 `global-preflight` 仲裁后直接失败;0.9.33 在 `approval-load` 后 `DIAGNOSE` 且无独立 `gate-failed` 事件。诊断不取得计划、恢复、替代、发布或 `TEST` 权威,不基于目录结构、时间戳、错误文本或非权威状态推断归属。
|
|
86
|
+
|
|
87
|
+
针对 reconcile 失败,仅核实 0.9.22 与 0.9.27 两个 writer 的两条旧记录,分别归属于 0.22.0 与 0.25.0 目标;其 `sourceRunId` 与 `sourceRunDigest` 必须与当前项目原父运行一致,子事件流须符合已审 writer 的完整必要前缀,且 `source-run-load passed` 与 `reconcile started` 必须指向同一父运行,每个 `actionId`/`actionType` 必须对应父计划原动作。仅在完整核验后,方可确认其真实目标并排除替代阻断,失败状态本身不构成零外写,也不赋予恢复、替代或 TEST 权威。
|
|
88
|
+
|
|
89
|
+
针对 verify 失败,仅 0.9.7 与 0.9.12 两条记录可被用于核实真实目标归属,其唯一父引用取自真实 `source-run-load passed` 的 `sourceRunId`,不接受补造 `sourceRunDigest`,仅接受已核实 writer 的完整有限前后缀。通过当前项目原 `runs` 内父文件,复用 `loadRun`、现行计划 Schema、不可变计划路径、完整谱系与检查点映射,确认父运行 `PUBLISHED`,再根据父计划核实其为 `foundation-adoption-review@0.1.0`,与 0.28.0 不同。每个动作身份必须对应父计划;同目标、不完整父权威、未知阶段、关系冲突或矛盾仍拒。其中 0.9.7 实际执行过隔离消费者安装,包含 `execute` 及本地写入,不能声称“零 execute”或“零本地写入”,亦不得由此授予发布外写权限。verify 失败不构成 publish 零外写证明,不赋予发布、恢复、替代或 TEST 权威。
|
|
90
|
+
|
|
91
|
+
所有未知、损坏、矛盾、不连续、封口不符或关系冲突的历史记录一律不可用。原件不得删除、搬移、补字段或重建。自引用、环、多有效后继或坏后继均不得使旧计划复活。
|
|
92
|
+
|
|
93
|
+
替代操作不放宽来源提交与远端 main 的精确相等要求、`TEST` 状态、远端冲突或批准门限制。继续精确执行 Foundation 0.25.0 机制,目标归属由 Release Skill 领域责任判断,不依赖目录名、时间或错误文本推断。本轮未删除或重命名产品能力,替代仅作用于符合条件的运行计划。未发布候选或本地自测不代表发布成功,亦不构成独立验收。所有操作均基于 0.9.38 支持入口,不能撤销任意旧软件或绕过现有协议。
|
package/src/commands/approve.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
2
|
+
import { assertPlanNotReplaced } from '../core/plan-replacement.mjs';
|
|
1
3
|
/**
|
|
2
4
|
* Approve command: record approval for a frozen release plan.
|
|
3
5
|
*
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
17
|
-
import { resolve, dirname, basename } from 'node:path';
|
|
19
|
+
import { resolve, dirname, basename, sep } from 'node:path';
|
|
18
20
|
|
|
19
21
|
import {
|
|
20
22
|
assertAuthorityFileTarget,
|
|
@@ -276,33 +278,40 @@ export async function approvePlan(options) {
|
|
|
276
278
|
actualDigest,
|
|
277
279
|
`${approvalDigest}.json`,
|
|
278
280
|
);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
const replacementRoot = options.root ?? (plan.production || plan.testAcceptance || Object.hasOwn(plan, 'replacesPlanDigest')
|
|
282
|
+
|| resolve(planPath).split(sep).includes('.release-skill') ? projectRootFromPlanPath(planPath) : null);
|
|
283
|
+
const replacementLock = replacementRoot
|
|
284
|
+
? await acquireProjectLock({ root: replacementRoot, command: 'approve', mode: 'exclusive' }) : null;
|
|
281
285
|
try {
|
|
282
|
-
await
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
if (replacementRoot) await assertPlanNotReplaced(replacementRoot, { ...plan, digest: actualDigest }, planPath);
|
|
287
|
+
await prepareAuthorityDirectory(dirname(immutableApprovalPath));
|
|
288
|
+
await assertAuthorityFileTarget(immutableApprovalPath);
|
|
289
|
+
try {
|
|
290
|
+
await writeFile(immutableApprovalPath, json, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code !== 'EEXIST') throw error;
|
|
293
|
+
const existing = await readFile(immutableApprovalPath, 'utf8');
|
|
294
|
+
if (existing !== json) {
|
|
295
|
+
throw new ReleaseError(
|
|
296
|
+
GATE_FAILED,
|
|
297
|
+
'approval authority digest collision: existing bytes differ',
|
|
298
|
+
{ planDigest: actualDigest, approvalDigest, immutableApprovalPath },
|
|
299
|
+
);
|
|
300
|
+
}
|
|
292
301
|
}
|
|
293
|
-
}
|
|
294
302
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
303
|
+
const writePath = outputPath ?? resolve(releaseDir, 'approval-record.json');
|
|
304
|
+
await prepareAuthorityDirectory(dirname(writePath));
|
|
305
|
+
await assertAuthorityFileTarget(writePath);
|
|
306
|
+
await writeFile(writePath, json, 'utf8');
|
|
299
307
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
return Object.freeze({
|
|
309
|
+
...approvalRecord,
|
|
310
|
+
approvalDigest,
|
|
311
|
+
approvalPath: immutableApprovalPath,
|
|
312
|
+
latestApprovalPath: writePath,
|
|
313
|
+
});
|
|
314
|
+
} finally { if (replacementLock) await replacementLock.release(); }
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
/**
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindReplacement } from '../core/plan-replacement.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Prepare command: freeze a release plan with snapshots and gates.
|
|
3
4
|
*
|
|
@@ -3448,6 +3449,7 @@ async function reattachTestCandidate({
|
|
|
3448
3449
|
expectedSubjectDigest: options.expectedSubjectDigest,
|
|
3449
3450
|
configDigest,
|
|
3450
3451
|
extraDrifts,
|
|
3452
|
+
replaceUnpublishedPlan: options.replaceUnpublishedPlan,
|
|
3451
3453
|
});
|
|
3452
3454
|
const savedPrepared = adoptedCandidate.candidate.prepared;
|
|
3453
3455
|
await recheckCandidateClosure(realRoot, savedPrepared, config, savedOptions.unitIds);
|
|
@@ -3624,6 +3626,7 @@ async function reattachTestCandidate({
|
|
|
3624
3626
|
const latestPlanPath = output ?? resolve(releaseDir, 'release-plan.json');
|
|
3625
3627
|
const plannedDigest = computePlanDigest(plan);
|
|
3626
3628
|
const immutablePlanPath = resolve(dirname(latestPlanPath), 'plans', `${plannedDigest}.json`);
|
|
3629
|
+
await bindReplacement({ root: realRoot, prepared: decision.prepared, resultFile: requirements.resultFile });
|
|
3627
3630
|
const { planPath: writtenPath, planDigest } = await writePlanImmutable(immutablePlanPath, plan);
|
|
3628
3631
|
await writePlanAtomic(latestPlanPath, plan);
|
|
3629
3632
|
await writeFrozenMarker(releaseDir, {
|
|
@@ -3781,6 +3784,9 @@ export async function prepareRelease(options) {
|
|
|
3781
3784
|
|
|
3782
3785
|
const { config, configPath, configDigest } = await loadProjectConfig({ root: realRoot });
|
|
3783
3786
|
assertTestIntakeDeclaration({ config, production });
|
|
3787
|
+
if (options.replaceUnpublishedPlan !== undefined && (!production || !config.testIntake)) {
|
|
3788
|
+
throw new ReleaseError(CONFIG_INVALID, 'unpublished replacement requires production testIntake');
|
|
3789
|
+
}
|
|
3784
3790
|
if (options?.candidatePath && !config.testIntake) {
|
|
3785
3791
|
throw new ReleaseError(CONFIG_INVALID, 'candidate path requires testIntake', {
|
|
3786
3792
|
reason: 'TEST_CANDIDATE_WITHOUT_INTAKE',
|
|
@@ -5424,10 +5430,11 @@ export async function prepareRelease(options) {
|
|
|
5424
5430
|
config,
|
|
5425
5431
|
selectedUnitIds: releaseSelection.selectedUnitIds,
|
|
5426
5432
|
});
|
|
5433
|
+
const boundPrepared = await bindReplacement({ root: realRoot, prepared: projectPrepared(plan), replaceUnpublishedPlan: options.replaceUnpublishedPlan, resultFile: requirementsNow.resultFile });
|
|
5427
5434
|
const saved = await writeTestCandidate({
|
|
5428
5435
|
root: realRoot,
|
|
5429
5436
|
runDir,
|
|
5430
|
-
prepared:
|
|
5437
|
+
prepared: boundPrepared,
|
|
5431
5438
|
requirements: requirementsNow,
|
|
5432
5439
|
prepareOptions: {
|
|
5433
5440
|
workflow,
|
|
@@ -5439,7 +5446,7 @@ export async function prepareRelease(options) {
|
|
|
5439
5446
|
),
|
|
5440
5447
|
},
|
|
5441
5448
|
});
|
|
5442
|
-
const subjectDigest = subjectDigestOf(
|
|
5449
|
+
const subjectDigest = subjectDigestOf(boundPrepared);
|
|
5443
5450
|
await evidence.append({
|
|
5444
5451
|
phase: 'test-candidate',
|
|
5445
5452
|
status: 'saved',
|
package/src/commands/publish.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
2
|
+
import { assertPlanNotReplaced } from '../core/plan-replacement.mjs';
|
|
1
3
|
/**
|
|
2
4
|
* Publish command: Saga-pattern checkpoint execution with safety gates.
|
|
3
5
|
*
|
|
@@ -498,6 +500,7 @@ export async function publishRelease(options) {
|
|
|
498
500
|
}
|
|
499
501
|
validatePlan(plan);
|
|
500
502
|
assertImmutablePlanAuthority(planPath, plan);
|
|
503
|
+
await assertPlanNotReplaced(root, plan, planPath);
|
|
501
504
|
await verifyTestAcceptance({ root, plan });
|
|
502
505
|
const isProductionPlan = plan.production?.mode === 'github-npm-v1';
|
|
503
506
|
|
|
@@ -1248,8 +1251,13 @@ export async function publishRelease(options) {
|
|
|
1248
1251
|
...(finishedAt ? { finishedAt } : {}),
|
|
1249
1252
|
});
|
|
1250
1253
|
let stateSequence = 0;
|
|
1251
|
-
|
|
1252
|
-
|
|
1254
|
+
const replacementLock = await acquireProjectLock({ root, command: 'publish', mode: 'exclusive' });
|
|
1255
|
+
let latestState;
|
|
1256
|
+
try {
|
|
1257
|
+
await assertPlanNotReplaced(root, plan, planPath);
|
|
1258
|
+
latestState = await appendRunState(runDir, stateSequence, buildPersistedState());
|
|
1259
|
+
recoveryRunPath = latestState.statePath;
|
|
1260
|
+
} finally { await replacementLock.release(); }
|
|
1253
1261
|
|
|
1254
1262
|
await evidence.append({
|
|
1255
1263
|
phase: 'publish',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertPlanNotReplaced } from '../core/plan-replacement.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Reconcile command: idempotent recovery from partial publish.
|
|
3
4
|
*
|
|
@@ -149,6 +150,7 @@ export async function reconcileRelease(options) {
|
|
|
149
150
|
}
|
|
150
151
|
validatePlan(plan);
|
|
151
152
|
assertImmutablePlanAuthority(planPath, plan);
|
|
153
|
+
await assertPlanNotReplaced(root, plan, planPath);
|
|
152
154
|
await verifyTestAcceptance({ root, plan });
|
|
153
155
|
const isProductionPlan = plan.production?.mode === 'github-npm-v1';
|
|
154
156
|
|
package/src/commands/route.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isProvenPreAuthorityFailure } from '../core/pre-authority-failure.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* release-route command: Deterministic quickstart routing for workflow profiles.
|
|
3
4
|
*
|
|
@@ -46,7 +47,6 @@ import {
|
|
|
46
47
|
validateRunCheckpointMapping,
|
|
47
48
|
} from '../core/run.mjs';
|
|
48
49
|
import { computePlanDigest, validatePlan } from '../core/plan.mjs';
|
|
49
|
-
import { resolveProducerVersion } from '../core/evidence.mjs';
|
|
50
50
|
import { normalizePostPublishView, postPublishActionId } from '../core/postpublish.mjs';
|
|
51
51
|
|
|
52
52
|
const WORKFLOW_KINDS = Object.freeze([
|
|
@@ -732,170 +732,13 @@ async function inspectStateDirectory(statesPath) {
|
|
|
732
732
|
return { ok: true, entries };
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
-
async function isProvenPreAuthorityFailure(runDir, runName) {
|
|
736
|
-
if (!/^publish-/.test(runName)) return false;
|
|
737
|
-
const summaryPath = join(runDir, 'summary.json');
|
|
738
|
-
const evidencePath = join(runDir, 'evidence.jsonl');
|
|
739
|
-
let summary;
|
|
740
|
-
let events;
|
|
741
|
-
try {
|
|
742
|
-
summary = JSON.parse(await readFile(summaryPath, 'utf8'));
|
|
743
|
-
events = (await readFile(evidencePath, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
744
|
-
} catch {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// The v1 writer (used before the producer-bound v2 envelope) created the
|
|
749
|
-
// run directory and appended safety-gate events before its first
|
|
750
|
-
// `release-run.json` state. A failed global preflight then appended only a
|
|
751
|
-
// generic `publish` failure and sealed a summary. This branch deliberately
|
|
752
|
-
// accepts that historical shape only when every byte still proves the same
|
|
753
|
-
// pre-authority boundary. It never infers safety from a directory name,
|
|
754
|
-
// mtime, summary alone, or a loosely similar phase.
|
|
755
|
-
const legacyPreAuthorityPhases = new Set([
|
|
756
|
-
'safety-gate',
|
|
757
|
-
'global-preflight-arbitration',
|
|
758
|
-
're-observe-previous-public-baseline',
|
|
759
|
-
]);
|
|
760
|
-
const legacyInputFailure = /(?:global preflight failed|source authority .*failed|cannot read (?:release plan|approval record)|production (?:publish|commands) require|plan (?:digest mismatch|action completeness gate failed|is not valid|requires)|baseline (?:changed|check failed)|previous-public-baseline|adapter .*failed)/i;
|
|
761
|
-
const legacyUnsafeFailure = /(?:remote (?:tag|branch|release) .*already exists|human intervention required|remote conflict|external write)/i;
|
|
762
|
-
const legacyPreAuthorityGates = new Set([
|
|
763
|
-
'plan-load',
|
|
764
|
-
'plan-schema',
|
|
765
|
-
'plan-digest',
|
|
766
|
-
'action-completeness',
|
|
767
|
-
'approval-load',
|
|
768
|
-
'adapter-availability',
|
|
769
|
-
'baseline-check',
|
|
770
|
-
'source-authority',
|
|
771
|
-
]);
|
|
772
|
-
const validLegacyTimestamp = (value) => typeof value === 'string'
|
|
773
|
-
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
|
|
774
|
-
&& Number.isFinite(Date.parse(value));
|
|
775
|
-
const validLegacyError = (error) => error && typeof error === 'object' && !Array.isArray(error)
|
|
776
|
-
&& typeof error.code === 'string' && error.code.length > 0
|
|
777
|
-
&& (error.message === undefined || typeof error.message === 'string');
|
|
778
|
-
const validLegacyEvent = (event, runId) => event && typeof event === 'object' && !Array.isArray(event)
|
|
779
|
-
&& event.schemaVersion === 1
|
|
780
|
-
&& !Object.hasOwn(event, 'producer')
|
|
781
|
-
&& typeof event.runId === 'string' && event.runId === runId
|
|
782
|
-
&& Number.isSafeInteger(event.sequence) && event.sequence >= 1
|
|
783
|
-
&& validLegacyTimestamp(event.timestamp)
|
|
784
|
-
&& event.command === 'publish'
|
|
785
|
-
&& typeof event.phase === 'string'
|
|
786
|
-
&& (legacyPreAuthorityPhases.has(event.phase) || event.phase === 'publish')
|
|
787
|
-
&& typeof event.status === 'string' && event.status.length > 0
|
|
788
|
-
&& (event.error === undefined || event.error === null || validLegacyError(event.error))
|
|
789
|
-
&& (event.duration === undefined || Number.isSafeInteger(event.duration) && event.duration >= 0)
|
|
790
|
-
&& (event.details === undefined || event.details !== null
|
|
791
|
-
&& typeof event.details === 'object' && !Array.isArray(event.details));
|
|
792
|
-
const legacyRunId = basename(runDir);
|
|
793
|
-
const legacyTerminal = events.at(-1);
|
|
794
|
-
const legacyValid = summary && summary.status === 'FAILED'
|
|
795
|
-
&& validLegacyError(summary.error)
|
|
796
|
-
&& typeof summary.error.message === 'string' && legacyInputFailure.test(summary.error.message)
|
|
797
|
-
&& !legacyUnsafeFailure.test(summary.error.message)
|
|
798
|
-
&& (summary.recoveryActionCode === undefined || summary.recoveryActionCode === 'RETRY_COMMAND')
|
|
799
|
-
&& summary.runPath === undefined
|
|
800
|
-
&& summary.finalRunDigest === undefined
|
|
801
|
-
&& summary.latestStatePath === undefined
|
|
802
|
-
&& summary.checkpointStatuses === undefined
|
|
803
|
-
&& Array.isArray(events) && events.length >= 2
|
|
804
|
-
&& events.every((event, index) => validLegacyEvent(event, legacyRunId)
|
|
805
|
-
&& event.sequence === index + 1)
|
|
806
|
-
&& legacyTerminal.phase === 'publish'
|
|
807
|
-
&& legacyTerminal.status === 'failed'
|
|
808
|
-
&& validLegacyError(legacyTerminal.error)
|
|
809
|
-
&& legacyTerminal.error.code === summary.error.code
|
|
810
|
-
&& events.slice(0, -1).filter((event) => event.status === 'failed').every((event) =>
|
|
811
|
-
event.phase === 'safety-gate'
|
|
812
|
-
&& legacyPreAuthorityGates.has(event.gate)
|
|
813
|
-
&& validLegacyError(event.error))
|
|
814
|
-
&& !events.some((event) => event.phase === 'global-preflight-arbitration'
|
|
815
|
-
&& event.status !== 'pre-observe')
|
|
816
|
-
&& !events.slice(0, -1).some((event) => event.phase === 'publish'
|
|
817
|
-
|| /checkpoint|execute|postpublish/i.test(event.phase)
|
|
818
|
-
|| Object.hasOwn(event, 'prePersistedRunPath')
|
|
819
|
-
|| Object.hasOwn(event, 'checkpointCount')
|
|
820
|
-
|| Object.hasOwn(event, 'checkpointStatuses'));
|
|
821
|
-
if (legacyValid) return true;
|
|
822
|
-
|
|
823
|
-
const producerVersion = resolveProducerVersion();
|
|
824
|
-
const sameProducer = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
825
|
-
&& Object.keys(value).length === 2
|
|
826
|
-
&& value.name === 'release-skill' && value.version === producerVersion;
|
|
827
|
-
const envelopeKeys = new Set([
|
|
828
|
-
'schemaVersion', 'runId', 'sequence', 'timestamp', 'command', 'producer',
|
|
829
|
-
'phase', 'status', 'error', 'duration', 'details',
|
|
830
|
-
]);
|
|
831
|
-
const validTimestamp = (value) => typeof value === 'string'
|
|
832
|
-
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
|
|
833
|
-
&& Number.isFinite(Date.parse(value));
|
|
834
|
-
const detailValidators = {
|
|
835
|
-
unitId: (value) => typeof value === 'string',
|
|
836
|
-
gate: (value) => typeof value === 'string',
|
|
837
|
-
hookName: (value) => typeof value === 'string',
|
|
838
|
-
actionId: (value) => typeof value === 'string',
|
|
839
|
-
step: (value) => typeof value === 'string',
|
|
840
|
-
hookId: (value) => typeof value === 'string',
|
|
841
|
-
tagCommit: (value) => typeof value === 'string',
|
|
842
|
-
cached: (value) => typeof value === 'boolean',
|
|
843
|
-
testSelection: (value) => typeof value === 'string',
|
|
844
|
-
cacheKey: (value) => typeof value === 'string',
|
|
845
|
-
durationMs: (value) => value === null || Number.isSafeInteger(value) && value >= 0,
|
|
846
|
-
exitCode: (value) => Number.isSafeInteger(value),
|
|
847
|
-
findingCount: (value) => Number.isSafeInteger(value) && value >= 0,
|
|
848
|
-
};
|
|
849
|
-
const validDetails = (value) => value === undefined
|
|
850
|
-
|| value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
851
|
-
&& Object.entries(value).every(([key, detail]) => !detailValidators[key] || detailValidators[key](detail));
|
|
852
|
-
const validEnvelope = (event) => event && typeof event === 'object'
|
|
853
|
-
&& Object.keys(event).every((key) => envelopeKeys.has(key))
|
|
854
|
-
&& event.schemaVersion === 2
|
|
855
|
-
&& typeof event.runId === 'string' && event.runId === basename(runDir)
|
|
856
|
-
&& Number.isSafeInteger(event.sequence) && event.sequence >= 1
|
|
857
|
-
&& validTimestamp(event.timestamp)
|
|
858
|
-
&& event.command === 'publish'
|
|
859
|
-
&& typeof event.phase === 'string' && event.phase.length > 0
|
|
860
|
-
&& typeof event.status === 'string' && event.status.length > 0
|
|
861
|
-
&& sameProducer(event.producer)
|
|
862
|
-
&& (event.error === undefined || event.error === null
|
|
863
|
-
|| (typeof event.error === 'object' && !Array.isArray(event.error)
|
|
864
|
-
&& Object.keys(event.error).every((key) => key === 'code' || key === 'message')
|
|
865
|
-
&& typeof event.error.code === 'string' && event.error.code.length > 0
|
|
866
|
-
&& (event.error.message === undefined || typeof event.error.message === 'string')))
|
|
867
|
-
&& (event.duration === undefined || Number.isSafeInteger(event.duration) && event.duration >= 0)
|
|
868
|
-
&& validDetails(event.details);
|
|
869
|
-
if (summary.status !== 'FAILED' || summary.recoveryActionCode !== 'RETRY_COMMAND'
|
|
870
|
-
|| summary.evidencePath !== 'evidence.jsonl' || !sameProducer(summary.producer)
|
|
871
|
-
|| !Array.isArray(events) || events.length === 0) return false;
|
|
872
|
-
let previousSequence = 0;
|
|
873
|
-
let firstFailure = null;
|
|
874
|
-
let genericFailure = null;
|
|
875
|
-
for (const event of events) {
|
|
876
|
-
if (!validEnvelope(event) || event.sequence !== previousSequence + 1) return false;
|
|
877
|
-
previousSequence = event.sequence;
|
|
878
|
-
if (event.status === 'failed' && firstFailure === null) firstFailure = event;
|
|
879
|
-
if (event.phase === 'publish') {
|
|
880
|
-
if (event.status !== 'failed' || genericFailure !== null || event.details?.prePersistedRunPath
|
|
881
|
-
|| event.details?.recoveryActionCode !== summary.recoveryActionCode) return false;
|
|
882
|
-
genericFailure = event;
|
|
883
|
-
} else if (!['safety-gate', 'global-preflight-arbitration'].includes(event.phase)) {
|
|
884
|
-
return false;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
if (!firstFailure || !genericFailure || events.at(-1) !== genericFailure
|
|
888
|
-
|| summary.evidenceSequence !== firstFailure.sequence
|
|
889
|
-
|| summary.stablePhase !== firstFailure.phase) return false;
|
|
890
|
-
return !events.some((event) => event.details?.prePersistedRunPath);
|
|
891
|
-
}
|
|
892
735
|
|
|
893
736
|
/**
|
|
894
737
|
* Postverify is a downstream run type that predates the generic recovery
|
|
895
738
|
* reader. Its completed and open records are consumable only when the summary
|
|
896
739
|
* agrees with the sealed status and the verify parent is fully lineage-valid.
|
|
897
740
|
*/
|
|
898
|
-
async function readLegacyPostverifyRecovery(runPath) {
|
|
741
|
+
export async function readLegacyPostverifyRecovery(runPath) {
|
|
899
742
|
try {
|
|
900
743
|
const run = await loadRun(runPath, { requireDigest: true });
|
|
901
744
|
const acceptedStatuses = new Set(['DISTRIBUTED', 'PARTIAL', 'BLOCKED', 'NEEDS_INPUT']);
|