release-skill 0.9.0 → 0.9.3
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 +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +81 -0
- package/INSTALL.md +22 -9
- package/INSTALL.zh-CN.md +15 -8
- package/README.md +40 -35
- package/README.zh-CN.md +31 -29
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +110 -0
- package/adapters/claude/bin/error-codes.json +8 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/claude/schemas/release-project.schema.json +4 -0
- package/adapters/claude/schemas/release-run.schema.json +41 -1
- package/adapters/claude/skills/release-config/SKILL.md +5 -3
- package/adapters/claude/skills/release-docs/SKILL.md +4 -2
- package/adapters/claude/skills/release-finish/SKILL.md +6 -4
- package/adapters/claude/skills/release-help/SKILL.md +14 -5
- package/adapters/claude/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +4 -2
- package/adapters/claude/skills/release-publish/SKILL.md +19 -10
- package/adapters/claude/skills/release-verify/SKILL.md +2 -0
- package/adapters/claude/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/claude/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/claude/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +110 -0
- package/adapters/codex/bin/error-codes.json +8 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/codex/schemas/release-project.schema.json +4 -0
- package/adapters/codex/schemas/release-run.schema.json +41 -1
- package/adapters/codex/skills/release-config/SKILL.md +5 -3
- package/adapters/codex/skills/release-docs/SKILL.md +4 -2
- package/adapters/codex/skills/release-finish/SKILL.md +6 -4
- package/adapters/codex/skills/release-help/SKILL.md +14 -5
- package/adapters/codex/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +4 -2
- package/adapters/codex/skills/release-publish/SKILL.md +19 -10
- package/adapters/codex/skills/release-verify/SKILL.md +2 -0
- package/adapters/codex/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/codex/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/codex/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +110 -0
- package/adapters/kimi/bin/error-codes.json +8 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/kimi/schemas/release-project.schema.json +4 -0
- package/adapters/kimi/schemas/release-run.schema.json +41 -1
- package/adapters/kimi/skills/release-config/SKILL.md +5 -3
- package/adapters/kimi/skills/release-docs/SKILL.md +4 -2
- package/adapters/kimi/skills/release-finish/SKILL.md +6 -4
- package/adapters/kimi/skills/release-help/SKILL.md +14 -5
- package/adapters/kimi/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +4 -2
- package/adapters/kimi/skills/release-publish/SKILL.md +19 -10
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
- package/adapters/kimi/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/kimi/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/kimi/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +110 -0
- package/adapters/workbuddy/bin/error-codes.json +8 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +4 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +41 -1
- package/adapters/workbuddy/skills/release-config/SKILL.md +5 -3
- package/adapters/workbuddy/skills/release-docs/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-finish/SKILL.md +6 -4
- package/adapters/workbuddy/skills/release-help/SKILL.md +14 -5
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-publish/SKILL.md +19 -10
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
- package/adapters/workbuddy/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/workbuddy/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/workbuddy/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/bin/consumer-contract-vectors.json +110 -0
- package/bin/error-codes.json +8 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +72 -4
- package/bin/release-skill.bundle.mjs +4772 -2238
- package/bin/rules.json +1 -1
- package/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/package.json +5 -5
- package/platform-manifest.json +5 -5
- package/references/01-state-machine.md +16 -1
- package/references/05-evidence-and-errors.md +2 -2
- package/schemas/release-project.schema.json +4 -0
- package/schemas/release-run.schema.json +41 -1
- package/skills/release-config/SKILL.md +5 -3
- package/skills/release-docs/SKILL.md +4 -2
- package/skills/release-finish/SKILL.md +6 -4
- package/skills/release-help/SKILL.md +14 -5
- package/skills/release-marketplace/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +4 -2
- package/skills/release-publish/SKILL.md +19 -10
- package/skills/release-verify/SKILL.md +2 -0
- package/skills-src/release-config/SKILL.md +5 -3
- package/skills-src/release-docs/SKILL.md +4 -2
- package/skills-src/release-finish/SKILL.md +6 -4
- package/skills-src/release-help/SKILL.md +14 -5
- package/skills-src/release-marketplace/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +4 -2
- package/skills-src/release-publish/SKILL.md +19 -10
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/commands/post-release-local.mjs +249 -58
- package/src/commands/prepare.mjs +178 -9
- package/src/commands/route.mjs +597 -18
- package/src/commands/setup.mjs +2 -0
- package/src/commands/ship.mjs +143 -16
- package/src/commands/verify.mjs +21 -0
- package/src/core/adoption-assessment.mjs +59 -0
- package/src/core/derived-artifact-gates.mjs +32 -5
- package/src/core/foundation-plugin-verification.mjs +213 -1
- package/src/core/hook-cache.mjs +318 -26
- package/src/core/hooks.mjs +8 -1
- package/src/producers/foundation-resource-projection.mjs +16 -3
- package/src/schemas/executable-identity-observation.schema.json +162 -0
- package/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/src/schemas/migration-manifest.schema.json +271 -7
- package/src/schemas/plugin-verification-request.schema.json +147 -22
- package/src/schemas/plugin-verification-result.schema.json +436 -18
- package/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
package/src/commands/route.mjs
CHANGED
|
@@ -34,11 +34,20 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { execFileSync } from 'node:child_process';
|
|
37
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
38
|
-
import { resolve } from 'node:path';
|
|
39
|
-
import { ReleaseError } from '../core/errors.mjs';
|
|
37
|
+
import { lstat, readdir, readFile } from 'node:fs/promises';
|
|
38
|
+
import { relative, resolve, basename, dirname, join } from 'node:path';
|
|
39
|
+
import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
|
|
40
40
|
import { listReleaseTags } from './lineage.mjs';
|
|
41
41
|
import { readRunRecovery, renderRecoveryCommand } from '../core/recovery.mjs';
|
|
42
|
+
import {
|
|
43
|
+
loadRun,
|
|
44
|
+
validateRunLineage,
|
|
45
|
+
validateRunPlanDigest,
|
|
46
|
+
validateRunCheckpointMapping,
|
|
47
|
+
} from '../core/run.mjs';
|
|
48
|
+
import { computePlanDigest, validatePlan } from '../core/plan.mjs';
|
|
49
|
+
import { resolveProducerVersion } from '../core/evidence.mjs';
|
|
50
|
+
import { normalizePostPublishView, postPublishActionId } from '../core/postpublish.mjs';
|
|
42
51
|
|
|
43
52
|
const WORKFLOW_KINDS = Object.freeze([
|
|
44
53
|
'full-happy-end',
|
|
@@ -349,38 +358,129 @@ export async function classifyBaselineSurface(root, prevCommit) {
|
|
|
349
358
|
*/
|
|
350
359
|
export async function readRunRouting(root, options = {}) {
|
|
351
360
|
const cwd = resolve(root);
|
|
361
|
+
const targetVersion = options.targetVersion ?? null;
|
|
352
362
|
const runsDir = resolve(cwd, '.release-skill', 'runs');
|
|
353
363
|
let runDirs;
|
|
354
364
|
try {
|
|
355
365
|
runDirs = await readdir(runsDir);
|
|
356
366
|
} catch (error) {
|
|
357
367
|
if (error.code === 'ENOENT') return { hasPartialRun: false, recoveryActionCode: null, runs: [] };
|
|
358
|
-
|
|
368
|
+
const relativePath = relative(cwd, runsDir);
|
|
369
|
+
const diagnostic = {
|
|
370
|
+
code: error.code ?? 'RUNS_DIRECTORY_UNREADABLE',
|
|
371
|
+
message: `cannot read release runs directory: ${error.message}`,
|
|
372
|
+
relativePath,
|
|
373
|
+
nextActionCode: 'DIAGNOSE',
|
|
374
|
+
nextAction: '保留历史记录,先按该路径完成人工诊断,再决定正式恢复动作',
|
|
375
|
+
classification: 'UNKNOWN_LEGACY_OR_CORRUPT_AUTHORITY',
|
|
376
|
+
};
|
|
377
|
+
const run = {
|
|
378
|
+
runPath: runsDir,
|
|
379
|
+
recoveryActionCode: null,
|
|
380
|
+
diagnostic,
|
|
381
|
+
};
|
|
382
|
+
return {
|
|
383
|
+
hasPartialRun: false,
|
|
384
|
+
recoveryActionCode: null,
|
|
385
|
+
runs: [],
|
|
386
|
+
diagnostics: [{
|
|
387
|
+
path: relativePath,
|
|
388
|
+
runPath: runsDir,
|
|
389
|
+
classification: diagnostic.classification,
|
|
390
|
+
basis: diagnostic.message,
|
|
391
|
+
recoveryActionCode: null,
|
|
392
|
+
nextActionCode: 'DIAGNOSE',
|
|
393
|
+
nextAction: diagnostic.nextAction,
|
|
394
|
+
}],
|
|
395
|
+
};
|
|
359
396
|
}
|
|
360
397
|
const records = [];
|
|
398
|
+
const diagnostics = [];
|
|
361
399
|
for (const runDir of runDirs) {
|
|
362
400
|
let runPath = resolve(runsDir, runDir, 'release-run.json');
|
|
401
|
+
const runDirectory = resolve(runsDir, runDir);
|
|
402
|
+
let authorityExists = false;
|
|
403
|
+
let authorityMissing = false;
|
|
404
|
+
let markerlessRunDebris = false;
|
|
363
405
|
try {
|
|
364
406
|
await readFile(runPath);
|
|
407
|
+
authorityExists = true;
|
|
365
408
|
} catch (error) {
|
|
366
409
|
if (error.code === 'ENOENT') {
|
|
410
|
+
authorityMissing = true;
|
|
367
411
|
// Prepare/assess have evidence directories but no release-run contract.
|
|
368
412
|
if (/^(prepare|assess|hooks-validate)-/.test(runDir)) continue;
|
|
413
|
+
const statesPath = resolve(runsDir, runDir, 'states');
|
|
414
|
+
const stateInspection = await inspectStateDirectory(statesPath);
|
|
415
|
+
if (!stateInspection.ok) {
|
|
416
|
+
records.push({
|
|
417
|
+
runPath: stateInspection.path,
|
|
418
|
+
recoveryActionCode: 'DIAGNOSE',
|
|
419
|
+
diagnostic: {
|
|
420
|
+
code: stateInspection.code,
|
|
421
|
+
message: stateInspection.message,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
369
426
|
// An append-only sequence is ordered by its bound slot, never by time.
|
|
370
427
|
// readRunRecovery reuses validateRunLineage to check its predecessors.
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
428
|
+
const slots = stateInspection.entries.filter((name) => /^\d{6}\.json$/.test(name)).sort();
|
|
429
|
+
if (slots.length > 0) {
|
|
430
|
+
runPath = resolve(runsDir, runDir, 'states', slots.at(-1));
|
|
431
|
+
authorityExists = true;
|
|
432
|
+
}
|
|
374
433
|
}
|
|
375
434
|
}
|
|
376
|
-
|
|
435
|
+
if (!authorityExists && authorityMissing
|
|
436
|
+
&& await isProvenPreAuthorityFailure(runDirectory, runDir)) {
|
|
437
|
+
diagnostics.push({
|
|
438
|
+
path: relative(cwd, runDirectory),
|
|
439
|
+
relativePath: relative(cwd, runPath),
|
|
440
|
+
runPath,
|
|
441
|
+
classification: 'PRE_AUTHORITY_FAILURE',
|
|
442
|
+
basis: '版本化 evidence writer、发布命令顺序和缺少 states/release-run.json 共同证明未取得运行 authority,未进入 checkpoint execute',
|
|
443
|
+
recoveryActionCode: 'RETRY_COMMAND',
|
|
444
|
+
nextActionCode: 'RETRY_COMMAND',
|
|
445
|
+
nextAction: '修复失败原因后重新运行原命令;该目录不是发布 authority',
|
|
446
|
+
});
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const record = runDir.startsWith('postverify-') && authorityExists
|
|
450
|
+
? await readLegacyPostverifyRecovery(runPath)
|
|
451
|
+
: await readRunRecovery(runPath, options);
|
|
452
|
+
if (targetVersion && authorityMissing) {
|
|
453
|
+
markerlessRunDebris = await isMarkerlessRunDebris(runDirectory);
|
|
454
|
+
if (markerlessRunDebris) record.emptyRunDebris = true;
|
|
455
|
+
}
|
|
456
|
+
records.push(record);
|
|
457
|
+
}
|
|
458
|
+
const targetScopedRecords = [];
|
|
459
|
+
const historicalDiagnostics = [];
|
|
460
|
+
for (const record of records) {
|
|
461
|
+
// Without an explicit target there is no safe release authority to
|
|
462
|
+
// recover. Keep every finding visible, but leave workflow selection to
|
|
463
|
+
// diff/baseline classification. This prevents an old DIAGNOSE or PARTIAL
|
|
464
|
+
// record from becoming an implicit target or an authorization gate.
|
|
465
|
+
if (!targetVersion) {
|
|
466
|
+
historicalDiagnostics.push(buildHistoricalDiagnostic(cwd, record, null));
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (!(await classifyTargetScope(cwd, record, targetVersion))) {
|
|
470
|
+
historicalDiagnostics.push(buildHistoricalDiagnostic(cwd, record, targetVersion));
|
|
471
|
+
} else {
|
|
472
|
+
targetScopedRecords.push(record);
|
|
473
|
+
}
|
|
377
474
|
}
|
|
378
475
|
const identity = (run) => `${run.planDigest}:${run.runId}:${run.runDigest}`;
|
|
379
476
|
const consumed = new Set(records.filter((r) => r.run).flatMap((r) => r.lineage.map((edge) => identity(edge.run))));
|
|
380
477
|
const samePublication = (a, b) => a.planDigest === b.planDigest
|
|
381
478
|
&& a.sourceRunId === b.sourceRunId && a.sourceRunDigest === b.sourceRunDigest;
|
|
382
|
-
const unresolved =
|
|
383
|
-
|
|
479
|
+
const unresolved = targetScopedRecords.filter((record) => {
|
|
480
|
+
// A record is eligible for current recovery only after the existing
|
|
481
|
+
// reader has validated its run and its bound plan. Invalid, legacy, and
|
|
482
|
+
// damaged records remain diagnostics and can never authorize recovery.
|
|
483
|
+
if (!record.run || !record.plan) return false;
|
|
384
484
|
if (consumed.has(identity(record.run))) return false;
|
|
385
485
|
// Distribute retries point to the same publication, not to each other.
|
|
386
486
|
// Only a validated completed distribution can retire its failed sibling.
|
|
@@ -394,7 +494,14 @@ export async function readRunRouting(root, options = {}) {
|
|
|
394
494
|
return true;
|
|
395
495
|
});
|
|
396
496
|
const priority = ['DIAGNOSE', 'RECONCILE', 'DISTRIBUTE', 'VERIFY', 'RETRY_COMMAND'];
|
|
397
|
-
const
|
|
497
|
+
const actionable = unresolved.filter((record) =>
|
|
498
|
+
typeof record.recoveryActionCode === 'string' && record.recoveryActionCode.length > 0);
|
|
499
|
+
// Multiple valid unfinished authorities for one explicit target cannot be
|
|
500
|
+
// selected by directory order, mtime, or producer metadata. Surface all of
|
|
501
|
+
// them and require an operator to choose the exact lineage.
|
|
502
|
+
const recoveryActionCode = actionable.length > 1
|
|
503
|
+
? 'DIAGNOSE'
|
|
504
|
+
: priority.find((code) => unresolved.some((r) => r.recoveryActionCode === code)) ?? null;
|
|
398
505
|
const runs = unresolved.filter((r) => r.recoveryActionCode).map((r) => ({
|
|
399
506
|
runPath: r.runPath,
|
|
400
507
|
planPath: r.run?.planPath,
|
|
@@ -402,9 +509,447 @@ export async function readRunRouting(root, options = {}) {
|
|
|
402
509
|
status: r.run?.status,
|
|
403
510
|
recoveryActionCode: r.recoveryActionCode,
|
|
404
511
|
recoveryRunPath: r.recoveryRunPath,
|
|
405
|
-
...(r.diagnostic ? {
|
|
512
|
+
...(r.diagnostic ? {
|
|
513
|
+
diagnostic: {
|
|
514
|
+
...r.diagnostic,
|
|
515
|
+
relativePath: r.runPath ? relative(cwd, r.runPath) : null,
|
|
516
|
+
nextActionCode: 'DIAGNOSE',
|
|
517
|
+
nextAction: '保留历史记录,先按该路径完成人工诊断,再决定正式恢复动作',
|
|
518
|
+
classification: 'UNKNOWN_LEGACY_OR_CORRUPT_AUTHORITY',
|
|
519
|
+
},
|
|
520
|
+
} : {}),
|
|
406
521
|
})).sort((a, b) => a.runPath.localeCompare(b.runPath));
|
|
407
|
-
|
|
522
|
+
const unifiedDiagnostics = [
|
|
523
|
+
...diagnostics,
|
|
524
|
+
...historicalDiagnostics,
|
|
525
|
+
...runs.filter((run) => run.diagnostic).map((run) => ({
|
|
526
|
+
path: run.diagnostic.relativePath,
|
|
527
|
+
runPath: run.runPath,
|
|
528
|
+
classification: run.diagnostic.classification,
|
|
529
|
+
basis: run.diagnostic.message,
|
|
530
|
+
recoveryActionCode: run.recoveryActionCode,
|
|
531
|
+
nextActionCode: run.diagnostic.nextActionCode,
|
|
532
|
+
nextAction: run.diagnostic.nextAction,
|
|
533
|
+
})),
|
|
534
|
+
];
|
|
535
|
+
return {
|
|
536
|
+
hasPartialRun: unresolved.some((r) => r.run?.status === 'PARTIAL'),
|
|
537
|
+
recoveryActionCode,
|
|
538
|
+
runs,
|
|
539
|
+
...(unifiedDiagnostics.length > 0 ? { diagnostics: unifiedDiagnostics } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Resolve whether a validated record belongs to the explicitly requested
|
|
545
|
+
* target. Plans are the only release-domain target authority; evidence and
|
|
546
|
+
* directory names never substitute for a plan binding.
|
|
547
|
+
*/
|
|
548
|
+
function planTargetVersions(plan) {
|
|
549
|
+
return [...new Set((Array.isArray(plan?.units) ? plan.units : [])
|
|
550
|
+
.map((unit) => unit?.targetVersion)
|
|
551
|
+
.filter((version) => typeof version === 'string' && version.length > 0))];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function classifyTargetScope(cwd, record, targetVersion) {
|
|
555
|
+
if (record.emptyRunDebris) return false;
|
|
556
|
+
// Only the fully validated run + plan pair is a current-recovery
|
|
557
|
+
// candidate. A readable path, producer version, directory name, timestamp,
|
|
558
|
+
// summary, or error text cannot substitute for that authority.
|
|
559
|
+
if (!(record.run && record.plan)) return false;
|
|
560
|
+
const versions = planTargetVersions(record.plan);
|
|
561
|
+
return versions.length > 0 && versions.every((version) => version === targetVersion);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function buildHistoricalDiagnostic(cwd, record, targetVersion) {
|
|
565
|
+
const path = record.runPath ? relative(cwd, record.runPath) : null;
|
|
566
|
+
const versions = planTargetVersions(record.plan);
|
|
567
|
+
const binding = !targetVersion
|
|
568
|
+
? '未指定目标版本;该记录只能作为历史诊断,不能决定当前工作流'
|
|
569
|
+
: record.emptyRunDebris
|
|
570
|
+
? '目录内没有可识别 authority、summary、evidence 或 state 制品的空 debris'
|
|
571
|
+
: versions.length > 0
|
|
572
|
+
? `记录绑定版本 ${versions.join('、')}`
|
|
573
|
+
: '记录属于不能绑定目标版本的旧 authority 或 legacy schema';
|
|
574
|
+
return {
|
|
575
|
+
path,
|
|
576
|
+
runPath: record.runPath,
|
|
577
|
+
classification: 'HISTORICAL_OUT_OF_SCOPE',
|
|
578
|
+
basis: targetVersion
|
|
579
|
+
? `${binding},显式目标为 ${targetVersion};保留历史诊断,不参与目标版本阻断计数或下一动作`
|
|
580
|
+
: `${binding};保留历史诊断,不参与当前工作流或恢复动作`,
|
|
581
|
+
recoveryActionCode: record.recoveryActionCode ?? null,
|
|
582
|
+
nextActionCode: null,
|
|
583
|
+
nextAction: '仅作历史诊断,不参与当前目标版本路由',
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function isMarkerlessRunDebris(runDirectory) {
|
|
588
|
+
const knownConsumers = new Set(['claude', 'codex', 'kimi', 'codebuddy', 'workbuddy']);
|
|
589
|
+
const readDirectory = async (path) => {
|
|
590
|
+
try {
|
|
591
|
+
return await readdir(path, { withFileTypes: true });
|
|
592
|
+
} catch {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
const entries = await readDirectory(runDirectory);
|
|
597
|
+
if (entries === null) return false;
|
|
598
|
+
if (entries.length === 0) return true;
|
|
599
|
+
|
|
600
|
+
// A lifecycle run has its markers at the run root. The only markerless
|
|
601
|
+
// exception is the known consumer-install evidence layout, whose payload
|
|
602
|
+
// is nested below `evidence/` (with optional consumer homes beside it).
|
|
603
|
+
const allowed = new Set(['evidence', 'consumers']);
|
|
604
|
+
if (entries.some((entry) => !allowed.has(entry.name) || !entry.isDirectory() || entry.isSymbolicLink())) return false;
|
|
605
|
+
const evidence = entries.find((entry) => entry.name === 'evidence');
|
|
606
|
+
if (!evidence) return false;
|
|
607
|
+
const evidenceEntries = await readDirectory(join(runDirectory, 'evidence'));
|
|
608
|
+
if (!evidenceEntries || evidenceEntries.length === 0) return false;
|
|
609
|
+
for (const evidenceEntry of evidenceEntries) {
|
|
610
|
+
if (!evidenceEntry.isDirectory() || evidenceEntry.isSymbolicLink()
|
|
611
|
+
|| !/^[a-z][a-z0-9-]*$/i.test(evidenceEntry.name)) return false;
|
|
612
|
+
const evidenceFiles = await readDirectory(join(runDirectory, 'evidence', evidenceEntry.name));
|
|
613
|
+
if (!evidenceFiles || evidenceFiles.length !== 1
|
|
614
|
+
|| evidenceFiles[0].name !== 'release-skill-install-evidence.json'
|
|
615
|
+
|| !evidenceFiles[0].isFile() || evidenceFiles[0].isSymbolicLink()) return false;
|
|
616
|
+
try {
|
|
617
|
+
const evidenceRecord = JSON.parse(await readFile(
|
|
618
|
+
join(runDirectory, 'evidence', evidenceEntry.name, evidenceFiles[0].name),
|
|
619
|
+
'utf8',
|
|
620
|
+
));
|
|
621
|
+
if (!evidenceRecord || typeof evidenceRecord !== 'object' || Array.isArray(evidenceRecord)
|
|
622
|
+
|| !knownConsumers.has(evidenceRecord.consumer)
|
|
623
|
+
|| typeof evidenceRecord.plugin !== 'string' || evidenceRecord.plugin.length === 0
|
|
624
|
+
|| typeof evidenceRecord.version !== 'string' || evidenceRecord.version.length === 0) return false;
|
|
625
|
+
} catch {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const consumers = entries.find((entry) => entry.name === 'consumers');
|
|
631
|
+
if (consumers) {
|
|
632
|
+
const consumerEntries = await readDirectory(join(runDirectory, 'consumers'));
|
|
633
|
+
if (!consumerEntries || consumerEntries.length === 0) return false;
|
|
634
|
+
for (const consumerEntry of consumerEntries) {
|
|
635
|
+
const consumer = consumerEntry.name.split('-', 1)[0];
|
|
636
|
+
if (!consumerEntry.isDirectory() || consumerEntry.isSymbolicLink()
|
|
637
|
+
|| !knownConsumers.has(consumer)) return false;
|
|
638
|
+
const homeEntries = await readDirectory(join(runDirectory, 'consumers', consumerEntry.name));
|
|
639
|
+
if (!homeEntries || homeEntries.some((entry) => !entry.isDirectory() || entry.isSymbolicLink()
|
|
640
|
+
|| !knownConsumers.has(entry.name.slice(1)))) return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* The publish writer creates the run directory before evidence, but writes
|
|
648
|
+
* its first immutable run state only after global preflight and immediately
|
|
649
|
+
* before checkpoint execution. Its final catch appends one generic failed
|
|
650
|
+
* `publish` event; when no authority exists, all preceding events must still
|
|
651
|
+
* be pre-authority phases. A current v2 summary/evidence pair with that exact
|
|
652
|
+
* shape therefore proves a failed, non-authoritative attempt. Legacy v1
|
|
653
|
+
* streams use the same writer order but have no producer envelope; they are
|
|
654
|
+
* accepted only when their older, looser event shape still proves the same
|
|
655
|
+
* boundary. Summaries without matching evidence and any stream that reached
|
|
656
|
+
* a persisted `publish` or checkpoint event remain diagnostic.
|
|
657
|
+
*/
|
|
658
|
+
async function inspectStateDirectory(statesPath) {
|
|
659
|
+
let directoryStat;
|
|
660
|
+
try {
|
|
661
|
+
directoryStat = await lstat(statesPath);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
if (error.code === 'ENOENT') return { ok: true, entries: [] };
|
|
664
|
+
return {
|
|
665
|
+
ok: false,
|
|
666
|
+
path: statesPath,
|
|
667
|
+
code: 'STATE_DIRECTORY_UNREADABLE',
|
|
668
|
+
message: `cannot inspect run states directory: ${error.message}`,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
if (!directoryStat.isDirectory()) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
path: statesPath,
|
|
675
|
+
code: 'STATE_DIRECTORY_INVALID',
|
|
676
|
+
message: 'run states path is not a regular directory',
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
let entries;
|
|
680
|
+
try {
|
|
681
|
+
entries = await readdir(statesPath);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
return {
|
|
684
|
+
ok: false,
|
|
685
|
+
path: statesPath,
|
|
686
|
+
code: 'STATE_DIRECTORY_UNREADABLE',
|
|
687
|
+
message: `cannot read run states directory: ${error.message}`,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
for (const entry of entries) {
|
|
691
|
+
let entryStat;
|
|
692
|
+
const entryPath = join(statesPath, entry);
|
|
693
|
+
try {
|
|
694
|
+
entryStat = await lstat(entryPath);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return {
|
|
697
|
+
ok: false,
|
|
698
|
+
path: entryPath,
|
|
699
|
+
code: 'STATE_ENTRY_UNREADABLE',
|
|
700
|
+
message: `cannot inspect run state entry: ${error.message}`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (!/^\d{6}\.json$/.test(entry) || !entryStat.isFile() || entryStat.isSymbolicLink()) {
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
path: entryPath,
|
|
707
|
+
code: 'STATE_ENTRY_INVALID',
|
|
708
|
+
message: 'run states must contain only regular files named NNNNNN.json',
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return { ok: true, entries };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function isProvenPreAuthorityFailure(runDir, runName) {
|
|
716
|
+
if (!/^publish-/.test(runName)) return false;
|
|
717
|
+
const summaryPath = join(runDir, 'summary.json');
|
|
718
|
+
const evidencePath = join(runDir, 'evidence.jsonl');
|
|
719
|
+
let summary;
|
|
720
|
+
let events;
|
|
721
|
+
try {
|
|
722
|
+
summary = JSON.parse(await readFile(summaryPath, 'utf8'));
|
|
723
|
+
events = (await readFile(evidencePath, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
724
|
+
} catch {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// The v1 writer (used before the producer-bound v2 envelope) created the
|
|
729
|
+
// run directory and appended safety-gate events before its first
|
|
730
|
+
// `release-run.json` state. A failed global preflight then appended only a
|
|
731
|
+
// generic `publish` failure and sealed a summary. This branch deliberately
|
|
732
|
+
// accepts that historical shape only when every byte still proves the same
|
|
733
|
+
// pre-authority boundary. It never infers safety from a directory name,
|
|
734
|
+
// mtime, summary alone, or a loosely similar phase.
|
|
735
|
+
const legacyPreAuthorityPhases = new Set([
|
|
736
|
+
'safety-gate',
|
|
737
|
+
'global-preflight-arbitration',
|
|
738
|
+
're-observe-previous-public-baseline',
|
|
739
|
+
]);
|
|
740
|
+
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;
|
|
741
|
+
const legacyUnsafeFailure = /(?:remote (?:tag|branch|release) .*already exists|human intervention required|remote conflict|external write)/i;
|
|
742
|
+
const legacyPreAuthorityGates = new Set([
|
|
743
|
+
'plan-load',
|
|
744
|
+
'plan-schema',
|
|
745
|
+
'plan-digest',
|
|
746
|
+
'action-completeness',
|
|
747
|
+
'approval-load',
|
|
748
|
+
'adapter-availability',
|
|
749
|
+
'baseline-check',
|
|
750
|
+
'source-authority',
|
|
751
|
+
]);
|
|
752
|
+
const validLegacyTimestamp = (value) => typeof value === 'string'
|
|
753
|
+
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
|
|
754
|
+
&& Number.isFinite(Date.parse(value));
|
|
755
|
+
const validLegacyError = (error) => error && typeof error === 'object' && !Array.isArray(error)
|
|
756
|
+
&& typeof error.code === 'string' && error.code.length > 0
|
|
757
|
+
&& (error.message === undefined || typeof error.message === 'string');
|
|
758
|
+
const validLegacyEvent = (event, runId) => event && typeof event === 'object' && !Array.isArray(event)
|
|
759
|
+
&& event.schemaVersion === 1
|
|
760
|
+
&& !Object.hasOwn(event, 'producer')
|
|
761
|
+
&& typeof event.runId === 'string' && event.runId === runId
|
|
762
|
+
&& Number.isSafeInteger(event.sequence) && event.sequence >= 1
|
|
763
|
+
&& validLegacyTimestamp(event.timestamp)
|
|
764
|
+
&& event.command === 'publish'
|
|
765
|
+
&& typeof event.phase === 'string'
|
|
766
|
+
&& (legacyPreAuthorityPhases.has(event.phase) || event.phase === 'publish')
|
|
767
|
+
&& typeof event.status === 'string' && event.status.length > 0
|
|
768
|
+
&& (event.error === undefined || event.error === null || validLegacyError(event.error))
|
|
769
|
+
&& (event.duration === undefined || Number.isSafeInteger(event.duration) && event.duration >= 0)
|
|
770
|
+
&& (event.details === undefined || event.details !== null
|
|
771
|
+
&& typeof event.details === 'object' && !Array.isArray(event.details));
|
|
772
|
+
const legacyRunId = basename(runDir);
|
|
773
|
+
const legacyTerminal = events.at(-1);
|
|
774
|
+
const legacyValid = summary && summary.status === 'FAILED'
|
|
775
|
+
&& validLegacyError(summary.error)
|
|
776
|
+
&& typeof summary.error.message === 'string' && legacyInputFailure.test(summary.error.message)
|
|
777
|
+
&& !legacyUnsafeFailure.test(summary.error.message)
|
|
778
|
+
&& (summary.recoveryActionCode === undefined || summary.recoveryActionCode === 'RETRY_COMMAND')
|
|
779
|
+
&& summary.runPath === undefined
|
|
780
|
+
&& summary.finalRunDigest === undefined
|
|
781
|
+
&& summary.latestStatePath === undefined
|
|
782
|
+
&& summary.checkpointStatuses === undefined
|
|
783
|
+
&& Array.isArray(events) && events.length >= 2
|
|
784
|
+
&& events.every((event, index) => validLegacyEvent(event, legacyRunId)
|
|
785
|
+
&& event.sequence === index + 1)
|
|
786
|
+
&& legacyTerminal.phase === 'publish'
|
|
787
|
+
&& legacyTerminal.status === 'failed'
|
|
788
|
+
&& validLegacyError(legacyTerminal.error)
|
|
789
|
+
&& legacyTerminal.error.code === summary.error.code
|
|
790
|
+
&& events.slice(0, -1).filter((event) => event.status === 'failed').every((event) =>
|
|
791
|
+
event.phase === 'safety-gate'
|
|
792
|
+
&& legacyPreAuthorityGates.has(event.gate)
|
|
793
|
+
&& validLegacyError(event.error))
|
|
794
|
+
&& !events.some((event) => event.phase === 'global-preflight-arbitration'
|
|
795
|
+
&& event.status !== 'pre-observe')
|
|
796
|
+
&& !events.slice(0, -1).some((event) => event.phase === 'publish'
|
|
797
|
+
|| /checkpoint|execute|postpublish/i.test(event.phase)
|
|
798
|
+
|| Object.hasOwn(event, 'prePersistedRunPath')
|
|
799
|
+
|| Object.hasOwn(event, 'checkpointCount')
|
|
800
|
+
|| Object.hasOwn(event, 'checkpointStatuses'));
|
|
801
|
+
if (legacyValid) return true;
|
|
802
|
+
|
|
803
|
+
const producerVersion = resolveProducerVersion();
|
|
804
|
+
const sameProducer = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
805
|
+
&& Object.keys(value).length === 2
|
|
806
|
+
&& value.name === 'release-skill' && value.version === producerVersion;
|
|
807
|
+
const envelopeKeys = new Set([
|
|
808
|
+
'schemaVersion', 'runId', 'sequence', 'timestamp', 'command', 'producer',
|
|
809
|
+
'phase', 'status', 'error', 'duration', 'details',
|
|
810
|
+
]);
|
|
811
|
+
const validTimestamp = (value) => typeof value === 'string'
|
|
812
|
+
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
|
|
813
|
+
&& Number.isFinite(Date.parse(value));
|
|
814
|
+
const detailValidators = {
|
|
815
|
+
unitId: (value) => typeof value === 'string',
|
|
816
|
+
gate: (value) => typeof value === 'string',
|
|
817
|
+
hookName: (value) => typeof value === 'string',
|
|
818
|
+
actionId: (value) => typeof value === 'string',
|
|
819
|
+
step: (value) => typeof value === 'string',
|
|
820
|
+
hookId: (value) => typeof value === 'string',
|
|
821
|
+
tagCommit: (value) => typeof value === 'string',
|
|
822
|
+
cached: (value) => typeof value === 'boolean',
|
|
823
|
+
testSelection: (value) => typeof value === 'string',
|
|
824
|
+
cacheKey: (value) => typeof value === 'string',
|
|
825
|
+
durationMs: (value) => value === null || Number.isSafeInteger(value) && value >= 0,
|
|
826
|
+
exitCode: (value) => Number.isSafeInteger(value),
|
|
827
|
+
findingCount: (value) => Number.isSafeInteger(value) && value >= 0,
|
|
828
|
+
};
|
|
829
|
+
const validDetails = (value) => value === undefined
|
|
830
|
+
|| value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
831
|
+
&& Object.entries(value).every(([key, detail]) => !detailValidators[key] || detailValidators[key](detail));
|
|
832
|
+
const validEnvelope = (event) => event && typeof event === 'object'
|
|
833
|
+
&& Object.keys(event).every((key) => envelopeKeys.has(key))
|
|
834
|
+
&& event.schemaVersion === 2
|
|
835
|
+
&& typeof event.runId === 'string' && event.runId === basename(runDir)
|
|
836
|
+
&& Number.isSafeInteger(event.sequence) && event.sequence >= 1
|
|
837
|
+
&& validTimestamp(event.timestamp)
|
|
838
|
+
&& event.command === 'publish'
|
|
839
|
+
&& typeof event.phase === 'string' && event.phase.length > 0
|
|
840
|
+
&& typeof event.status === 'string' && event.status.length > 0
|
|
841
|
+
&& sameProducer(event.producer)
|
|
842
|
+
&& (event.error === undefined || event.error === null
|
|
843
|
+
|| (typeof event.error === 'object' && !Array.isArray(event.error)
|
|
844
|
+
&& Object.keys(event.error).every((key) => key === 'code' || key === 'message')
|
|
845
|
+
&& typeof event.error.code === 'string' && event.error.code.length > 0
|
|
846
|
+
&& (event.error.message === undefined || typeof event.error.message === 'string')))
|
|
847
|
+
&& (event.duration === undefined || Number.isSafeInteger(event.duration) && event.duration >= 0)
|
|
848
|
+
&& validDetails(event.details);
|
|
849
|
+
if (summary.status !== 'FAILED' || summary.recoveryActionCode !== 'RETRY_COMMAND'
|
|
850
|
+
|| summary.evidencePath !== 'evidence.jsonl' || !sameProducer(summary.producer)
|
|
851
|
+
|| !Array.isArray(events) || events.length === 0) return false;
|
|
852
|
+
let previousSequence = 0;
|
|
853
|
+
let firstFailure = null;
|
|
854
|
+
let genericFailure = null;
|
|
855
|
+
for (const event of events) {
|
|
856
|
+
if (!validEnvelope(event) || event.sequence !== previousSequence + 1) return false;
|
|
857
|
+
previousSequence = event.sequence;
|
|
858
|
+
if (event.status === 'failed' && firstFailure === null) firstFailure = event;
|
|
859
|
+
if (event.phase === 'publish') {
|
|
860
|
+
if (event.status !== 'failed' || genericFailure !== null || event.details?.prePersistedRunPath
|
|
861
|
+
|| event.details?.recoveryActionCode !== summary.recoveryActionCode) return false;
|
|
862
|
+
genericFailure = event;
|
|
863
|
+
} else if (!['safety-gate', 'global-preflight-arbitration'].includes(event.phase)) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (!firstFailure || !genericFailure || events.at(-1) !== genericFailure
|
|
868
|
+
|| summary.evidenceSequence !== firstFailure.sequence
|
|
869
|
+
|| summary.stablePhase !== firstFailure.phase) return false;
|
|
870
|
+
return !events.some((event) => event.details?.prePersistedRunPath);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Postverify is a downstream run type that predates the generic recovery
|
|
875
|
+
* reader. A terminal legacy record is consumable only when its summary agrees
|
|
876
|
+
* with its sealed status and its verify parent is fully lineage-valid.
|
|
877
|
+
*/
|
|
878
|
+
async function readLegacyPostverifyRecovery(runPath) {
|
|
879
|
+
try {
|
|
880
|
+
const run = await loadRun(runPath, { requireDigest: true });
|
|
881
|
+
if (run.command !== 'postverify' || run.status !== 'DISTRIBUTED') {
|
|
882
|
+
throw new ReleaseError(GATE_FAILED, 'legacy postverify status is not terminal DISTRIBUTED');
|
|
883
|
+
}
|
|
884
|
+
const planRaw = await readFile(run.planPath, 'utf8');
|
|
885
|
+
const plan = JSON.parse(planRaw);
|
|
886
|
+
validatePlan(plan);
|
|
887
|
+
if (!plan.digest || plan.digest !== computePlanDigest(plan)) {
|
|
888
|
+
throw new ReleaseError(GATE_FAILED, 'legacy postverify plan digest is not intact');
|
|
889
|
+
}
|
|
890
|
+
validateRunPlanDigest(run, plan, { planPath: run.planPath });
|
|
891
|
+
const production = Boolean(plan.production);
|
|
892
|
+
if (production) {
|
|
893
|
+
// Production run authority is digest-addressed and must remain under
|
|
894
|
+
// the plan's sibling runs/ directory, exactly as recovery consumes it.
|
|
895
|
+
await loadRun(runPath, { requireDigest: true, authorityPlanPath: run.planPath });
|
|
896
|
+
}
|
|
897
|
+
if (!run.sourceRunPath || !run.sourceRunId || !run.sourceRunDigest) {
|
|
898
|
+
throw new ReleaseError(GATE_FAILED, 'legacy postverify is missing complete verify parent lineage');
|
|
899
|
+
}
|
|
900
|
+
const parent = await loadRun(run.sourceRunPath, {
|
|
901
|
+
requireDigest: true,
|
|
902
|
+
...(production ? { authorityPlanPath: run.planPath } : {}),
|
|
903
|
+
});
|
|
904
|
+
if (parent.command !== 'verify' || parent.status !== 'VERIFIED'
|
|
905
|
+
|| parent.runId !== run.sourceRunId || parent.runDigest !== run.sourceRunDigest) {
|
|
906
|
+
throw new ReleaseError(GATE_FAILED, 'legacy postverify requires the same-lineage VERIFIED verify parent');
|
|
907
|
+
}
|
|
908
|
+
validateRunCheckpointMapping(parent, plan.externalActions ?? []);
|
|
909
|
+
if (parent.checkpoints.some((checkpoint) => !['succeeded', 'skipped', 'NO_CHANGE'].includes(checkpoint.status))) {
|
|
910
|
+
throw new ReleaseError(GATE_FAILED, 'legacy postverify verify parent has incomplete checkpoints');
|
|
911
|
+
}
|
|
912
|
+
await validateRunLineage(parent, {
|
|
913
|
+
plan,
|
|
914
|
+
planPath: run.planPath,
|
|
915
|
+
runPath: run.sourceRunPath,
|
|
916
|
+
production,
|
|
917
|
+
});
|
|
918
|
+
const summary = JSON.parse(await readFile(join(resolve(runPath, '..'), 'summary.json'), 'utf8'));
|
|
919
|
+
if (!summary || summary.status !== run.status) {
|
|
920
|
+
throw new ReleaseError(GATE_FAILED, 'legacy postverify summary status does not match its sealed run status');
|
|
921
|
+
}
|
|
922
|
+
const expectedPostVerifyActions = normalizePostPublishView(plan).flatMap((declaration) => (
|
|
923
|
+
(declaration.hooks ?? [])
|
|
924
|
+
.filter((hook) => (hook.phase ?? 'distribute') === 'postVerify')
|
|
925
|
+
.map((hook) => ({
|
|
926
|
+
id: postPublishActionId({
|
|
927
|
+
planVersion: plan.planVersion,
|
|
928
|
+
unitId: declaration.unitId,
|
|
929
|
+
localId: hook.id,
|
|
930
|
+
}),
|
|
931
|
+
type: 'postpublish-hook',
|
|
932
|
+
}))
|
|
933
|
+
));
|
|
934
|
+
validateRunCheckpointMapping(run, expectedPostVerifyActions);
|
|
935
|
+
if (run.checkpoints.some((checkpoint) => !['succeeded', 'skipped', 'NO_CHANGE'].includes(checkpoint.status))) {
|
|
936
|
+
throw new ReleaseError(GATE_FAILED, 'legacy postverify DISTRIBUTED run has incomplete checkpoints');
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
runPath: resolve(runPath),
|
|
940
|
+
run,
|
|
941
|
+
plan,
|
|
942
|
+
lineage: [{ run: parent, runPath: resolve(run.sourceRunPath) }],
|
|
943
|
+
recoveryActionCode: null,
|
|
944
|
+
legacyPostverify: true,
|
|
945
|
+
};
|
|
946
|
+
} catch (error) {
|
|
947
|
+
return {
|
|
948
|
+
runPath: resolve(runPath),
|
|
949
|
+
recoveryActionCode: 'DIAGNOSE',
|
|
950
|
+
diagnostic: { code: error.code ?? GATE_FAILED, message: error.message },
|
|
951
|
+
};
|
|
952
|
+
}
|
|
408
953
|
}
|
|
409
954
|
|
|
410
955
|
export async function hasPartialRun(root) {
|
|
@@ -434,7 +979,10 @@ export function recommendWorkflow(diffClassification, baselineSurface, targetVer
|
|
|
434
979
|
const { code, docs, config, marketplace, mixed } = diffClassification;
|
|
435
980
|
|
|
436
981
|
const recovery = options.recovery;
|
|
437
|
-
|
|
982
|
+
// Recovery advice is target-bound. With no explicit target, route must
|
|
983
|
+
// remain a diff/baseline workflow recommendation even when diagnostics
|
|
984
|
+
// contain old PARTIAL or DIAGNOSE records.
|
|
985
|
+
if (targetVersion && recovery?.recoveryActionCode) {
|
|
438
986
|
const action = recovery.recoveryActionCode;
|
|
439
987
|
const selected = recovery.runs.find((run) => run.recoveryActionCode === action);
|
|
440
988
|
const recoveryRunPath = selected?.recoveryRunPath ?? selected?.runPath;
|
|
@@ -444,13 +992,20 @@ export function recommendWorkflow(diffClassification, baselineSurface, targetVer
|
|
|
444
992
|
reason: action === 'DIAGNOSE' ? '运行权威、冲突或批准尚需诊断;保留既有记录,不开始新发布'
|
|
445
993
|
: action === 'RETRY_COMMAND' ? '已校验为零写入阻断;修复原因后重试原命令'
|
|
446
994
|
: '根据已校验的计划、运行和血缘继续未完成阶段',
|
|
447
|
-
firstCommand: renderRecoveryCommand(action, { planPath: selected?.planPath, runPath: recoveryRunPath })
|
|
448
|
-
|
|
995
|
+
firstCommand: renderRecoveryCommand(action, { planPath: selected?.planPath, runPath: recoveryRunPath })
|
|
996
|
+
?? `无自动安全恢复入口;按 recoveryActionCode=${action} 处理既有记录`,
|
|
997
|
+
routing: {
|
|
998
|
+
rule: 1,
|
|
999
|
+
hasPartialRun,
|
|
1000
|
+
recoveryActionCode: action,
|
|
1001
|
+
runs: recovery.runs,
|
|
1002
|
+
...(recovery.diagnostics?.length > 0 ? { diagnostics: recovery.diagnostics } : {}),
|
|
1003
|
+
},
|
|
449
1004
|
};
|
|
450
1005
|
}
|
|
451
1006
|
|
|
452
1007
|
// Rule 1: PARTIAL run → reconcile first (highest priority).
|
|
453
|
-
if (hasPartialRun) {
|
|
1008
|
+
if (targetVersion && hasPartialRun) {
|
|
454
1009
|
return {
|
|
455
1010
|
workflowKind: 'reconcile',
|
|
456
1011
|
reason: '存在 PARTIAL run,必须先 reconcile 再开始任何新操作',
|
|
@@ -632,6 +1187,24 @@ function formatRecommendationText(recommendation) {
|
|
|
632
1187
|
if (recommendation.requiresAuthorization) {
|
|
633
1188
|
lines.push('Note: 远程写入(publish)未授权——prepare 只读冻结计划,publish 需另行授权。', '');
|
|
634
1189
|
}
|
|
1190
|
+
const diagnostics = recommendation.routing?.diagnostics ?? [
|
|
1191
|
+
...(recommendation.routing?.runs ?? [])
|
|
1192
|
+
.filter((run) => run.diagnostic)
|
|
1193
|
+
.map((run) => ({ ...run.diagnostic, path: run.diagnostic.relativePath })),
|
|
1194
|
+
];
|
|
1195
|
+
if (diagnostics.length > 0) {
|
|
1196
|
+
lines.push('Diagnostics:');
|
|
1197
|
+
for (const diagnostic of diagnostics) {
|
|
1198
|
+
lines.push(` - 路径: ${diagnostic.path ?? diagnostic.relativePath ?? 'unknown'}`);
|
|
1199
|
+
lines.push(` 分类: ${diagnostic.classification ?? 'UNKNOWN_LEGACY_OR_CORRUPT_AUTHORITY'}`);
|
|
1200
|
+
lines.push(` 动作: ${diagnostic.nextActionCode ?? diagnostic.recoveryActionCode ?? 'DIAGNOSE'}`);
|
|
1201
|
+
if (diagnostic.nextAction) lines.push(` 下一步: ${diagnostic.nextAction}`);
|
|
1202
|
+
if ((diagnostic.nextActionCode ?? diagnostic.recoveryActionCode) === 'DIAGNOSE') {
|
|
1203
|
+
lines.push(' 无自动安全恢复入口');
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
lines.push('');
|
|
1207
|
+
}
|
|
635
1208
|
return lines.join('\n');
|
|
636
1209
|
}
|
|
637
1210
|
|
|
@@ -700,7 +1273,7 @@ export default async function main(args = [], options = {}) {
|
|
|
700
1273
|
} else {
|
|
701
1274
|
baselineSurface = { status: 'indeterminable', kind: null, categories: {}, paths: [] };
|
|
702
1275
|
}
|
|
703
|
-
const recovery = await readRunRouting(root);
|
|
1276
|
+
const recovery = await readRunRouting(root, { targetVersion });
|
|
704
1277
|
const partial = recovery.hasPartialRun;
|
|
705
1278
|
const recommendation = recommendWorkflow(
|
|
706
1279
|
classification,
|
|
@@ -709,6 +1282,12 @@ export default async function main(args = [], options = {}) {
|
|
|
709
1282
|
partial,
|
|
710
1283
|
{ publishAuthorized, recovery },
|
|
711
1284
|
);
|
|
1285
|
+
if (recovery.diagnostics?.length > 0) {
|
|
1286
|
+
recommendation.routing = {
|
|
1287
|
+
...recommendation.routing,
|
|
1288
|
+
diagnostics: recovery.diagnostics,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
712
1291
|
|
|
713
1292
|
if (json) {
|
|
714
1293
|
console.log(JSON.stringify({
|