release-skill 0.8.0 → 0.9.0
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 +65 -7
- package/INSTALL.md +14 -2
- package/INSTALL.zh-CN.md +12 -2
- package/README.md +51 -31
- package/README.zh-CN.md +33 -20
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/prebuild-manifest.json +8 -4
- package/adapters/claude/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/claude/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/claude/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/claude/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/claude/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/release-plan.schema.json +43 -5
- package/adapters/claude/schemas/release-run.schema.json +35 -1
- package/adapters/claude/skills/release-finish/SKILL.md +65 -0
- package/adapters/claude/skills/release-verify/SKILL.md +6 -1
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/claude/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/claude/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/prebuild-manifest.json +8 -4
- package/adapters/codex/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/codex/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/codex/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/codex/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/codex/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/release-plan.schema.json +43 -5
- package/adapters/codex/schemas/release-run.schema.json +35 -1
- package/adapters/codex/skills/release-finish/SKILL.md +72 -0
- package/adapters/codex/skills/release-verify/SKILL.md +6 -1
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/codex/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/codex/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/prebuild-manifest.json +8 -4
- package/adapters/kimi/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/kimi/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/kimi/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/kimi/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/kimi/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/release-plan.schema.json +43 -5
- package/adapters/kimi/schemas/release-run.schema.json +35 -1
- package/adapters/kimi/skills/release-finish/SKILL.md +72 -0
- package/adapters/kimi/skills/release-verify/SKILL.md +6 -1
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/kimi/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/kimi/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/prebuild-manifest.json +8 -4
- package/adapters/workbuddy/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/release-plan.schema.json +43 -5
- package/adapters/workbuddy/schemas/release-run.schema.json +35 -1
- package/adapters/workbuddy/skills/release-finish/SKILL.md +65 -0
- package/adapters/workbuddy/skills/release-verify/SKILL.md +6 -1
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/workbuddy/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/workbuddy/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/bin/error-codes.json +1 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/prebuild-manifest.json +8 -4
- package/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +126 -1
- package/bin/release-skill-local-finish.mjs +4 -0
- package/bin/release-skill.bundle.mjs +7152 -4231
- package/bin/rules.json +1 -1
- package/package.json +4 -4
- package/platform-manifest.json +30 -4
- package/schemas/release-plan.schema.json +43 -5
- package/schemas/release-run.schema.json +35 -1
- package/skills/release-finish/SKILL.md +65 -0
- package/skills/release-verify/SKILL.md +6 -1
- package/skills-src/release-finish/SKILL.md +65 -0
- package/skills-src/release-verify/SKILL.md +6 -1
- package/src/commands/approve.mjs +14 -2
- package/src/commands/distribute.mjs +562 -321
- package/src/commands/post-release-local.mjs +611 -0
- package/src/commands/postverify.mjs +262 -155
- package/src/commands/prepare.mjs +83 -57
- package/src/commands/publish.mjs +11 -10
- package/src/commands/reconcile.mjs +11 -10
- package/src/commands/setup.mjs +26 -13
- package/src/commands/ship.mjs +45 -12
- package/src/commands/verify.mjs +81 -18
- package/src/core/adoption-assessment.mjs +0 -3
- package/src/core/approval.mjs +11 -8
- package/src/core/foundation-plugin-verification.mjs +247 -0
- package/src/core/hooks.mjs +0 -16
- package/src/core/plan.mjs +19 -11
- package/src/core/postpublish-approval.mjs +12 -2
- package/src/core/postpublish-bundle.mjs +51 -13
- package/src/core/postpublish.mjs +146 -6
- package/src/core/preset-executor.mjs +53 -0
- package/src/core/proposal-inbox.mjs +91 -27
- package/src/core/recovery.mjs +27 -12
- package/src/core/skill-resource-closure.mjs +8 -4
- package/src/producers/build-adapters.mjs +10 -1
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/src/schemas/host-verification-result.schema.json +2 -2
- package/src/schemas/plugin-verification-request.schema.json +374 -0
- package/src/schemas/plugin-verification-result.schema.json +799 -0
- package/src/schemas/watchdog-termination-envelope.schema.json +16 -0
|
@@ -99,16 +99,19 @@ import {
|
|
|
99
99
|
validatePostPublishDeclaration,
|
|
100
100
|
buildPostPublishContext,
|
|
101
101
|
normalizePostPublishDeclaration,
|
|
102
|
+
normalizePostPublishView,
|
|
102
103
|
orderNormalizedHooks,
|
|
103
104
|
effectiveHookRequiresApproval,
|
|
105
|
+
postPublishActionId,
|
|
106
|
+
validatePostPublishHookIdUniqueness,
|
|
104
107
|
POSTPUBLISH_CONTEXT_ENV,
|
|
105
108
|
PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
106
109
|
} from '../core/postpublish.mjs';
|
|
107
|
-
import { verifyAndInstallExecutionBundle } from '../core/postpublish-bundle.mjs';
|
|
110
|
+
import { verifyAndInstallExecutionBundle, verifyExecutionBundle } from '../core/postpublish-bundle.mjs';
|
|
108
111
|
import { projectPublicPayload, PROJECTION_MECHANISM, PUBLIC_PAYLOAD_DIRNAME } from '../core/postpublish-projection.mjs';
|
|
109
112
|
import { assertPostPublishApprovalAuthority, validatePostPublishApproval } from '../core/postpublish-approval.mjs';
|
|
110
113
|
import { readRunRecovery } from '../core/recovery.mjs';
|
|
111
|
-
import { executePresetHook } from '../core/preset-executor.mjs';
|
|
114
|
+
import { executePresetHook, preflightPresetHook } from '../core/preset-executor.mjs';
|
|
112
115
|
import {
|
|
113
116
|
ReleaseError,
|
|
114
117
|
GATE_FAILED,
|
|
@@ -325,7 +328,19 @@ export async function distributeRelease(options) {
|
|
|
325
328
|
{ expected: plan.digest, actual: actualDigest },
|
|
326
329
|
);
|
|
327
330
|
}
|
|
328
|
-
|
|
331
|
+
// v3 contract (§4.3): postPublish is a frozen declaration ARRAY. Every
|
|
332
|
+
// downstream read goes through the single read-only normalization entry
|
|
333
|
+
// normalizePostPublishView — never a direct single-object read. Legacy
|
|
334
|
+
// (planVersion 1/2) absent stays fail-closed exactly as before; a v3 empty
|
|
335
|
+
// array is legal and means "no post-release execution" (handled right after
|
|
336
|
+
// the run authority is allocated, below).
|
|
337
|
+
const declarations = normalizePostPublishView(plan);
|
|
338
|
+
// Rework R-02: the single array-level explicit-hook-id uniqueness authority
|
|
339
|
+
// (core/postpublish.mjs) re-asserts the frozen view here — a digest-correct,
|
|
340
|
+
// schema-valid plan with duplicate hook ids across units must fail before
|
|
341
|
+
// any run authority, approval consumption, or side effect.
|
|
342
|
+
validatePostPublishHookIdUniqueness(declarations);
|
|
343
|
+
if (declarations.length === 0 && plan.planVersion !== 3) {
|
|
329
344
|
throw new ReleaseError(
|
|
330
345
|
GATE_FAILED,
|
|
331
346
|
'release plan has no postPublish declaration; nothing to distribute',
|
|
@@ -352,6 +367,17 @@ export async function distributeRelease(options) {
|
|
|
352
367
|
let sourceRunDigest = null;
|
|
353
368
|
let finalRecordWritten = false;
|
|
354
369
|
|
|
370
|
+
// Saga-level chain state (single run authority; §6 matrix classification).
|
|
371
|
+
// Declared up here so every failBlocked/catch path can classify PARTIAL vs
|
|
372
|
+
// BLOCKED from the external-success facts (rework R-01): an exception after
|
|
373
|
+
// a successful external checkpoint must never be recorded BLOCKED.
|
|
374
|
+
let stopped = false;
|
|
375
|
+
let stopReason = null; // 'TARGET' | 'HOOK' — which failure stopped the saga (rework R-03).
|
|
376
|
+
let failures = 0;
|
|
377
|
+
let pushedWrites = 0;
|
|
378
|
+
let awaitingApproval = 0;
|
|
379
|
+
let hookSuccesses = 0;
|
|
380
|
+
|
|
355
381
|
const startedAt = clockFn();
|
|
356
382
|
let checkpoints = [];
|
|
357
383
|
let stateSequence = -1;
|
|
@@ -401,11 +427,18 @@ export async function distributeRelease(options) {
|
|
|
401
427
|
return appendRunState(runDir, stateSequence, buildPersistedState(status));
|
|
402
428
|
};
|
|
403
429
|
|
|
404
|
-
/**
|
|
405
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Persist the failure run record once lineage is known (idempotent).
|
|
432
|
+
* Rework R-01: an exception after a successful external checkpoint enters
|
|
433
|
+
* PARTIAL (the success checkpoint is retained — never rolled back); a
|
|
434
|
+
* failure with zero external successes stays BLOCKED.
|
|
435
|
+
*/
|
|
436
|
+
const recordFailure = async () => {
|
|
406
437
|
if (finalRecordWritten || !lineageKnown) return;
|
|
438
|
+
const externalCheckpointSuccesses = pushedWrites + hookSuccesses;
|
|
439
|
+
const status = externalCheckpointSuccesses > 0 ? PARTIAL : BLOCKED;
|
|
407
440
|
try {
|
|
408
|
-
await writeRunAtomic(runPath, buildPersistedState(
|
|
441
|
+
await writeRunAtomic(runPath, buildPersistedState(status, clockFn()));
|
|
409
442
|
finalRecordWritten = true;
|
|
410
443
|
} catch {
|
|
411
444
|
// Persistence must never mask the primary failure.
|
|
@@ -503,47 +536,105 @@ export async function distributeRelease(options) {
|
|
|
503
536
|
sourceRunStatus: sourceRun.status,
|
|
504
537
|
});
|
|
505
538
|
|
|
539
|
+
if (declarations.length === 0) {
|
|
540
|
+
// v3 zero declarations (§6 matrix row 0): the post-release phase is
|
|
541
|
+
// skipped — no adapter call, no hook, no external write. The run keeps
|
|
542
|
+
// the full safety-gate chain (plan, approval, source-run lineage) and
|
|
543
|
+
// records an honest DISTRIBUTED skip bound to the source run.
|
|
544
|
+
await evidence.append({
|
|
545
|
+
phase: 'postpublish',
|
|
546
|
+
status: 'skipped',
|
|
547
|
+
reason: 'ZERO_DECLARATIONS',
|
|
548
|
+
planVersion: plan.planVersion,
|
|
549
|
+
});
|
|
550
|
+
await snapshot(DISTRIBUTED);
|
|
551
|
+
await writeRunAtomic(runPath, buildPersistedState(DISTRIBUTED, clockFn()));
|
|
552
|
+
finalRecordWritten = true;
|
|
553
|
+
await evidence.append({
|
|
554
|
+
phase: 'distribute',
|
|
555
|
+
status: 'completed',
|
|
556
|
+
overallStatus: DISTRIBUTED,
|
|
557
|
+
checkpointStatuses: [],
|
|
558
|
+
});
|
|
559
|
+
await evidence.finish({ status: DISTRIBUTED, planPath, runPath, finishedAt: clockFn() });
|
|
560
|
+
return { planPath, runPath, status: DISTRIBUTED, checkpoints: [], recoveryActionCode: null };
|
|
561
|
+
}
|
|
562
|
+
|
|
506
563
|
// =======================================================================
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
564
|
+
// Phase A — full read-only preflight across ALL declarations (v3 contract
|
|
565
|
+
// §4.3): structure, identity, remote pre-facts and provided approvals are
|
|
566
|
+
// computed for every declaration BEFORE the first external write. A later
|
|
567
|
+
// declaration failing preflight means NO declaration may have produced an
|
|
568
|
+
// external write. Legacy plans present exactly one declaration (via
|
|
569
|
+
// normalizePostPublishView) and keep their exact behavior.
|
|
511
570
|
// =======================================================================
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
// their behavior ships).
|
|
524
|
-
const normalizedDeclaration = normalizePostPublishDeclaration(postPublish);
|
|
525
|
-
const orderedNormalizedHooks = orderNormalizedHooks(normalizedDeclaration.hooks);
|
|
526
|
-
await evidence.append({
|
|
527
|
-
phase: 'postpublish-normalization',
|
|
528
|
-
status: 'passed',
|
|
529
|
-
preGates: normalizedDeclaration.preGates.map((gate) => gate.gate),
|
|
530
|
-
hookCount: orderedNormalizedHooks.length,
|
|
531
|
-
hookIds: orderedNormalizedHooks.map((hook) => hook.id),
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
// postPublish hooks (v0.6.3 R1): distribute-phase hooks run in this saga;
|
|
535
|
-
// postVerify-phase hooks belong to the independent postVerify run (R3)
|
|
536
|
-
// and are only evidenced here — never executed, never silent.
|
|
537
|
-
const declaredHooks = postPublish.hooks ?? [];
|
|
538
|
-
const distributeHooks = declaredHooks.filter((hook) => (hook.phase ?? 'distribute') === 'distribute');
|
|
539
|
-
const deferredPostVerifyHooks = declaredHooks.length - distributeHooks.length;
|
|
540
|
-
|
|
541
|
-
/** Gate failure after lineage is known: persist BLOCKED, then rethrow. */
|
|
571
|
+
const v3 = plan.planVersion === 3;
|
|
572
|
+
// Checkpoint action ids are namespaced by the frozen unitId on v3 so a
|
|
573
|
+
// checkpoint stays addressable across units; legacy ids are unchanged
|
|
574
|
+
// (existing runs, tests and approvals pin the bare local ids). The
|
|
575
|
+
// derivation is shared core/postpublish.mjs `postPublishActionId` so
|
|
576
|
+
// recovery/verify mappings can never drift from the executed records.
|
|
577
|
+
const unitActionId = (unitId, localId) => postPublishActionId({ planVersion: plan.planVersion, unitId, localId });
|
|
578
|
+
|
|
579
|
+
/** Gate failure after lineage is known: persist the failure record
|
|
580
|
+
* (BLOCKED at zero external successes, PARTIAL once one exists — rework
|
|
581
|
+
* R-01), then rethrow. */
|
|
542
582
|
const failBlocked = async (error) => {
|
|
543
|
-
await
|
|
583
|
+
await recordFailure();
|
|
544
584
|
throw error;
|
|
545
585
|
};
|
|
546
586
|
|
|
587
|
+
// --- Phase A1: declaration structure validation + normalization ---
|
|
588
|
+
// R2: preset references resolve against the built-in preset registry
|
|
589
|
+
// (core/presets.mjs); per-preset config validation (dual addressing,
|
|
590
|
+
// marketplace/staticFiles shapes, secret scan) fails closed here — for
|
|
591
|
+
// EVERY declaration, before any adapter call or external write.
|
|
592
|
+
// R-01 (rework): the frozen execution bundle closure is re-verified for
|
|
593
|
+
// EVERY declaration here too (store re-read + Foundation closure
|
|
594
|
+
// recomputation, the exact implementation the Phase B install reuses) —
|
|
595
|
+
// a later declaration's missing/drifted bundle must fail the whole saga
|
|
596
|
+
// BEFORE unit A writes anything.
|
|
597
|
+
const prepared = [];
|
|
598
|
+
for (const declaration of declarations) {
|
|
599
|
+
validatePostPublishDeclaration(declaration, { unitId: declaration.unitId });
|
|
600
|
+
await verifyExecutionBundle({ planPath, postPublish: declaration });
|
|
601
|
+
const orderedTargets = orderTargetsByDependency(declaration.targets ?? []);
|
|
602
|
+
|
|
603
|
+
// Normalized hook table (design §2.2): every targets[] entry maps onto
|
|
604
|
+
// a preset hook (payload-mirror -> git-mirror, marketplace-index ->
|
|
605
|
+
// marketplace-index-render); the table is a deterministic projection of
|
|
606
|
+
// the digest-bound declaration, ordered by dependency topology +
|
|
607
|
+
// declaration order. Target execution below keeps the exact legacy
|
|
608
|
+
// semantics; hooks[] preset execution dispatches in the hook loop
|
|
609
|
+
// (proposal-inbox git-push is wired; other presets fail closed until
|
|
610
|
+
// their behavior ships).
|
|
611
|
+
const normalizedDeclaration = normalizePostPublishDeclaration(declaration);
|
|
612
|
+
const orderedNormalizedHooks = orderNormalizedHooks(normalizedDeclaration.hooks);
|
|
613
|
+
|
|
614
|
+
// postPublish hooks (v0.6.3 R1): distribute-phase hooks run in this
|
|
615
|
+
// saga; postVerify-phase hooks belong to the independent postVerify run
|
|
616
|
+
// (R3) and are only evidenced here — never executed, never silent.
|
|
617
|
+
const declaredHooks = declaration.hooks ?? [];
|
|
618
|
+
const distributeHooks = declaredHooks.filter((hook) => (hook.phase ?? 'distribute') === 'distribute');
|
|
619
|
+
const deferredPostVerifyHooks = declaredHooks.length - distributeHooks.length;
|
|
620
|
+
await evidence.append({
|
|
621
|
+
phase: 'postpublish-normalization',
|
|
622
|
+
status: 'passed',
|
|
623
|
+
unitId: declaration.unitId,
|
|
624
|
+
preGates: normalizedDeclaration.preGates.map((gate) => gate.gate),
|
|
625
|
+
hookCount: orderedNormalizedHooks.length,
|
|
626
|
+
hookIds: orderedNormalizedHooks.map((hook) => hook.id),
|
|
627
|
+
});
|
|
628
|
+
prepared.push({
|
|
629
|
+
declaration,
|
|
630
|
+
orderedTargets,
|
|
631
|
+
normalizedDeclaration,
|
|
632
|
+
orderedNormalizedHooks,
|
|
633
|
+
distributeHooks,
|
|
634
|
+
deferredPostVerifyHooks,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
547
638
|
// =======================================================================
|
|
548
639
|
// Gate: checkpoint approvals for requiresApproval hooks. Every provided
|
|
549
640
|
// record is validated fail-closed BEFORE any write: first the immutable
|
|
@@ -615,48 +706,53 @@ export async function distributeRelease(options) {
|
|
|
615
706
|
});
|
|
616
707
|
}
|
|
617
708
|
|
|
618
|
-
// Hooks whose checkpoint approval is still missing
|
|
709
|
+
// Hooks whose checkpoint approval is still missing (across ALL
|
|
710
|
+
// declarations; hook ids are globally unique). While any exist, the
|
|
619
711
|
// declared postPublish steps (unaudited project code) must not execute:
|
|
620
712
|
// the run parks at NEEDS_INPUT/PARTIAL and the approved reconcile rerun
|
|
621
713
|
// re-executes them. Targets remain plan-approval-authorized idempotent
|
|
622
714
|
// remote-state convergence and are unaffected.
|
|
623
|
-
const pendingHookApprovals = distributeHooks.filter(
|
|
715
|
+
const pendingHookApprovals = prepared.flatMap((prep) => prep.distributeHooks.filter(
|
|
624
716
|
(hook) => effectiveHookRequiresApproval(hook) && !approvedHookIds.has(hook.id),
|
|
625
|
-
);
|
|
717
|
+
));
|
|
626
718
|
|
|
627
|
-
// Checkpoint registry: one probe + one mirror per target
|
|
628
|
-
// then one postpublish-hook checkpoint per distribute-
|
|
719
|
+
// Checkpoint registry: per declaration, one probe + one mirror per target
|
|
720
|
+
// (declared order), then one postpublish-hook checkpoint per distribute-
|
|
721
|
+
// phase hook. Action ids are unit-namespaced on v3 (unitActionId).
|
|
629
722
|
checkpoints = [];
|
|
630
|
-
for (const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
723
|
+
for (const prep of prepared) {
|
|
724
|
+
const { declaration, orderedTargets, distributeHooks } = prep;
|
|
725
|
+
for (const target of orderedTargets) {
|
|
726
|
+
checkpoints.push({
|
|
727
|
+
actionId: unitActionId(declaration.unitId, `probe-${target.id}`),
|
|
728
|
+
actionType: ActionType.DISTRIBUTE_PROBE,
|
|
729
|
+
status: 'PENDING',
|
|
730
|
+
remoteUrl: target.remoteUrl,
|
|
731
|
+
branch: target.branch,
|
|
732
|
+
tag: declaration.tag,
|
|
733
|
+
executor: EXECUTOR,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
for (const target of orderedTargets) {
|
|
737
|
+
checkpoints.push({
|
|
738
|
+
actionId: unitActionId(declaration.unitId, target.id),
|
|
739
|
+
actionType: ActionType.DISTRIBUTE_MIRROR,
|
|
740
|
+
status: 'PENDING',
|
|
741
|
+
remoteUrl: target.remoteUrl,
|
|
742
|
+
branch: target.branch,
|
|
743
|
+
tag: declaration.tag,
|
|
744
|
+
tagCommit: SHA_RE.test(declaration.tagCommit ?? '') ? declaration.tagCommit : undefined,
|
|
745
|
+
executor: EXECUTOR,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
for (const hook of distributeHooks) {
|
|
749
|
+
checkpoints.push({
|
|
750
|
+
actionId: unitActionId(declaration.unitId, hook.id),
|
|
751
|
+
actionType: 'postpublish-hook',
|
|
752
|
+
status: 'PENDING',
|
|
753
|
+
executor: EXECUTOR,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
660
756
|
}
|
|
661
757
|
const checkpointById = new Map(checkpoints.map((cp) => [cp.actionId, cp]));
|
|
662
758
|
|
|
@@ -670,129 +766,150 @@ export async function distributeRelease(options) {
|
|
|
670
766
|
// local tag is diagnostic only (a fresh split source repo legitimately
|
|
671
767
|
// has no local tag). Missing binding/objects/lineage fails closed as
|
|
672
768
|
// GATE_FAILED; remote tag missing/drifted is a REMOTE_CONFLICT requiring
|
|
673
|
-
// human decision.
|
|
769
|
+
// human decision. Runs per declaration, in frozen order, all read-only.
|
|
674
770
|
// =======================================================================
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (postPublish.payloadSource !== PAYLOAD_SOURCE_TAG_WORKTREE) {
|
|
678
|
-
await failBlocked(new ReleaseError(
|
|
679
|
-
GATE_FAILED,
|
|
680
|
-
`postPublish.payloadSource must be "${PAYLOAD_SOURCE_TAG_WORKTREE}"`,
|
|
681
|
-
{ payloadSource: postPublish.payloadSource },
|
|
682
|
-
));
|
|
683
|
-
}
|
|
684
|
-
if (typeof postPublish.tagCommit !== 'string' || !SHA_RE.test(postPublish.tagCommit)) {
|
|
685
|
-
await failBlocked(new ReleaseError(
|
|
686
|
-
GATE_FAILED,
|
|
687
|
-
'postPublish is missing a frozen tagCommit binding; distribute fails closed',
|
|
688
|
-
{ tagCommit: postPublish.tagCommit ?? null },
|
|
689
|
-
));
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
let tagAuthority;
|
|
693
|
-
try {
|
|
694
|
-
tagAuthority = await resolveFrozenTagAuthority({
|
|
695
|
-
plan,
|
|
696
|
-
unitId: postPublish.unitId,
|
|
697
|
-
sourceRun,
|
|
698
|
-
root,
|
|
699
|
-
exec,
|
|
700
|
-
observeTagFn,
|
|
701
|
-
observeBranchFn,
|
|
702
|
-
// 裁决 15: the public main line is observed only for the explicit
|
|
703
|
-
// optional gate; with it off, no branch observation happens at all.
|
|
704
|
-
observeMainLine: postPublish.assertMainVersionAhead === true,
|
|
705
|
-
});
|
|
706
|
-
} catch (err) {
|
|
771
|
+
for (const prep of prepared) {
|
|
772
|
+
const { declaration } = prep;
|
|
707
773
|
await evidence.append({
|
|
708
774
|
phase: 'safety-gate',
|
|
709
775
|
gate: 'tag-identity',
|
|
710
|
-
status: '
|
|
711
|
-
|
|
712
|
-
details: {
|
|
713
|
-
code: err?.code ?? GATE_FAILED,
|
|
714
|
-
reason: err?.details?.reason ?? null,
|
|
715
|
-
},
|
|
716
|
-
});
|
|
717
|
-
await failBlocked(err instanceof ReleaseError ? err : new ReleaseError(
|
|
718
|
-
GATE_FAILED,
|
|
719
|
-
`tag identity cannot be verified: ${err?.message ?? err}`,
|
|
720
|
-
));
|
|
721
|
-
}
|
|
722
|
-
// Cross-section consistency: the postPublish block and the create-tag
|
|
723
|
-
// external action must agree on the frozen tag identity.
|
|
724
|
-
if (tagAuthority.tag !== postPublish.tag || tagAuthority.commit !== postPublish.tagCommit) {
|
|
725
|
-
await failBlocked(new ReleaseError(
|
|
726
|
-
GATE_FAILED,
|
|
727
|
-
'frozen postPublish tag identity conflicts with the frozen create-tag binding',
|
|
728
|
-
{
|
|
729
|
-
postPublishTag: postPublish.tag,
|
|
730
|
-
postPublishTagCommit: postPublish.tagCommit,
|
|
731
|
-
bindingTag: tagAuthority.tag,
|
|
732
|
-
bindingCommit: tagAuthority.commit,
|
|
733
|
-
},
|
|
734
|
-
));
|
|
735
|
-
}
|
|
736
|
-
if (tagAuthority.localTagDrifted) {
|
|
737
|
-
// R-01: the local tag is a diagnostic only — never a failure.
|
|
738
|
-
await evidence.append({
|
|
739
|
-
phase: 'safety-gate',
|
|
740
|
-
gate: 'tag-identity',
|
|
741
|
-
status: 'warning',
|
|
742
|
-
detail: 'local tag drifted from the frozen commit; public remote stays authoritative',
|
|
743
|
-
localTagCommit: tagAuthority.localTagCommit,
|
|
744
|
-
frozenTagCommit: tagAuthority.commit,
|
|
776
|
+
status: 'started',
|
|
777
|
+
unitId: declaration.unitId,
|
|
745
778
|
});
|
|
746
|
-
}
|
|
747
779
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
780
|
+
if (declaration.payloadSource !== PAYLOAD_SOURCE_TAG_WORKTREE) {
|
|
781
|
+
await failBlocked(new ReleaseError(
|
|
782
|
+
GATE_FAILED,
|
|
783
|
+
`postPublish.payloadSource must be "${PAYLOAD_SOURCE_TAG_WORKTREE}"`,
|
|
784
|
+
{ payloadSource: declaration.payloadSource, unitId: declaration.unitId },
|
|
785
|
+
));
|
|
786
|
+
}
|
|
787
|
+
if (typeof declaration.tagCommit !== 'string' || !SHA_RE.test(declaration.tagCommit)) {
|
|
788
|
+
await failBlocked(new ReleaseError(
|
|
789
|
+
GATE_FAILED,
|
|
790
|
+
'postPublish is missing a frozen tagCommit binding; distribute fails closed',
|
|
791
|
+
{ tagCommit: declaration.tagCommit ?? null, unitId: declaration.unitId },
|
|
792
|
+
));
|
|
793
|
+
}
|
|
756
794
|
|
|
757
|
-
|
|
758
|
-
// Gate 5 (optional): the public main line must have moved ahead of the
|
|
759
|
-
// tag (observed from the public remote, not the live workspace).
|
|
760
|
-
// =======================================================================
|
|
761
|
-
if (postPublish.assertMainVersionAhead === true) {
|
|
762
|
-
await evidence.append({ phase: 'safety-gate', gate: 'main-version-ahead', status: 'started' });
|
|
795
|
+
let tagAuthority;
|
|
763
796
|
try {
|
|
764
|
-
await
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
remoteUrl: tagAuthority.remoteUrl,
|
|
797
|
+
tagAuthority = await resolveFrozenTagAuthority({
|
|
798
|
+
plan,
|
|
799
|
+
unitId: declaration.unitId,
|
|
800
|
+
sourceRun,
|
|
801
|
+
root,
|
|
770
802
|
exec,
|
|
803
|
+
observeTagFn,
|
|
804
|
+
observeBranchFn,
|
|
805
|
+
// 裁决 15: the public main line is observed only for the explicit
|
|
806
|
+
// optional gate; with it off, no branch observation happens at all.
|
|
807
|
+
observeMainLine: declaration.assertMainVersionAhead === true,
|
|
771
808
|
});
|
|
772
809
|
} catch (err) {
|
|
773
810
|
await evidence.append({
|
|
774
811
|
phase: 'safety-gate',
|
|
775
|
-
gate: '
|
|
812
|
+
gate: 'tag-identity',
|
|
776
813
|
status: 'failed',
|
|
777
|
-
error: asError('
|
|
814
|
+
error: asError('TAG_IDENTITY_GATE_FAILED', boundedOutputTail(err?.message ?? String(err))),
|
|
815
|
+
details: {
|
|
816
|
+
code: err?.code ?? GATE_FAILED,
|
|
817
|
+
reason: err?.details?.reason ?? null,
|
|
818
|
+
unitId: declaration.unitId,
|
|
819
|
+
},
|
|
778
820
|
});
|
|
779
821
|
await failBlocked(err instanceof ReleaseError ? err : new ReleaseError(
|
|
780
822
|
GATE_FAILED,
|
|
781
|
-
`
|
|
823
|
+
`tag identity cannot be verified: ${err?.message ?? err}`,
|
|
824
|
+
));
|
|
825
|
+
}
|
|
826
|
+
// Cross-section consistency: the declaration and the create-tag
|
|
827
|
+
// external action must agree on the frozen tag identity.
|
|
828
|
+
if (tagAuthority.tag !== declaration.tag || tagAuthority.commit !== declaration.tagCommit) {
|
|
829
|
+
await failBlocked(new ReleaseError(
|
|
830
|
+
GATE_FAILED,
|
|
831
|
+
'frozen postPublish tag identity conflicts with the frozen create-tag binding',
|
|
832
|
+
{
|
|
833
|
+
postPublishTag: declaration.tag,
|
|
834
|
+
postPublishTagCommit: declaration.tagCommit,
|
|
835
|
+
bindingTag: tagAuthority.tag,
|
|
836
|
+
bindingCommit: tagAuthority.commit,
|
|
837
|
+
unitId: declaration.unitId,
|
|
838
|
+
},
|
|
782
839
|
));
|
|
783
840
|
}
|
|
784
|
-
|
|
841
|
+
if (tagAuthority.localTagDrifted) {
|
|
842
|
+
// R-01: the local tag is a diagnostic only — never a failure.
|
|
843
|
+
await evidence.append({
|
|
844
|
+
phase: 'safety-gate',
|
|
845
|
+
gate: 'tag-identity',
|
|
846
|
+
status: 'warning',
|
|
847
|
+
detail: 'local tag drifted from the frozen commit; public remote stays authoritative',
|
|
848
|
+
localTagCommit: tagAuthority.localTagCommit,
|
|
849
|
+
frozenTagCommit: tagAuthority.commit,
|
|
850
|
+
unitId: declaration.unitId,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
785
854
|
await evidence.append({
|
|
786
855
|
phase: 'safety-gate',
|
|
787
|
-
gate: '
|
|
856
|
+
gate: 'tag-identity',
|
|
788
857
|
status: 'passed',
|
|
789
|
-
|
|
790
|
-
|
|
858
|
+
tagCommit: tagAuthority.commit,
|
|
859
|
+
observedRemoteTag: tagAuthority.observedRemoteTag,
|
|
860
|
+
localTagPresent: tagAuthority.localTagPresent,
|
|
861
|
+
unitId: declaration.unitId,
|
|
791
862
|
});
|
|
863
|
+
|
|
864
|
+
// ===================================================================
|
|
865
|
+
// Gate 5 (optional): the public main line must have moved ahead of the
|
|
866
|
+
// tag (observed from the public remote, not the live workspace).
|
|
867
|
+
// ===================================================================
|
|
868
|
+
if (declaration.assertMainVersionAhead === true) {
|
|
869
|
+
await evidence.append({
|
|
870
|
+
phase: 'safety-gate',
|
|
871
|
+
gate: 'main-version-ahead',
|
|
872
|
+
status: 'started',
|
|
873
|
+
unitId: declaration.unitId,
|
|
874
|
+
});
|
|
875
|
+
try {
|
|
876
|
+
await assertMainLineAhead({
|
|
877
|
+
tagCommit: tagAuthority.commit,
|
|
878
|
+
branchCommit: tagAuthority.branchCommit,
|
|
879
|
+
branch: tagAuthority.branch,
|
|
880
|
+
gitDir: tagAuthority.gitDir,
|
|
881
|
+
remoteUrl: tagAuthority.remoteUrl,
|
|
882
|
+
exec,
|
|
883
|
+
});
|
|
884
|
+
} catch (err) {
|
|
885
|
+
await evidence.append({
|
|
886
|
+
phase: 'safety-gate',
|
|
887
|
+
gate: 'main-version-ahead',
|
|
888
|
+
status: 'failed',
|
|
889
|
+
error: asError('MAIN_VERSION_AHEAD_FAILED', boundedOutputTail(err?.message ?? String(err))),
|
|
890
|
+
});
|
|
891
|
+
await failBlocked(err instanceof ReleaseError ? err : new ReleaseError(
|
|
892
|
+
GATE_FAILED,
|
|
893
|
+
`assertMainVersionAhead failed: ${err?.message ?? err}`,
|
|
894
|
+
));
|
|
895
|
+
}
|
|
896
|
+
// failBlocked always throws, so this line runs only when the check passed.
|
|
897
|
+
await evidence.append({
|
|
898
|
+
phase: 'safety-gate',
|
|
899
|
+
gate: 'main-version-ahead',
|
|
900
|
+
status: 'passed',
|
|
901
|
+
branch: tagAuthority.branch,
|
|
902
|
+
branchCommit: tagAuthority.branchCommit,
|
|
903
|
+
unitId: declaration.unitId,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
prep.tagAuthority = tagAuthority;
|
|
792
908
|
}
|
|
793
909
|
|
|
794
910
|
// =======================================================================
|
|
795
|
-
// Gate 6: adapter availability + per-target reachability
|
|
911
|
+
// Gate 6: adapter availability (once, saga-level) + per-target reachability
|
|
912
|
+
// preflight (read-only) for EVERY declaration before any write.
|
|
796
913
|
// =======================================================================
|
|
797
914
|
let probeAdapter;
|
|
798
915
|
let mirrorAdapter;
|
|
@@ -806,99 +923,123 @@ export async function distributeRelease(options) {
|
|
|
806
923
|
));
|
|
807
924
|
}
|
|
808
925
|
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
targetId: target.id,
|
|
822
|
-
remoteUrl: target.remoteUrl,
|
|
823
|
-
tag: postPublish.tag,
|
|
824
|
-
};
|
|
825
|
-
let probeResult;
|
|
826
|
-
try {
|
|
827
|
-
probeResult = await probeAdapter.preflight(probeAction, {
|
|
828
|
-
externalWritesAuthorized: false,
|
|
829
|
-
plan,
|
|
830
|
-
root,
|
|
831
|
-
runDir,
|
|
926
|
+
for (const prep of prepared) {
|
|
927
|
+
const { declaration, orderedTargets } = prep;
|
|
928
|
+
const probeObservations = new Map();
|
|
929
|
+
for (const target of orderedTargets) {
|
|
930
|
+
const probeCheckpoint = checkpointById.get(unitActionId(declaration.unitId, `probe-${target.id}`));
|
|
931
|
+
probeCheckpoint.startedAt = clockFn();
|
|
932
|
+
await evidence.append({
|
|
933
|
+
phase: 'safety-gate',
|
|
934
|
+
gate: `probe-${target.id}`,
|
|
935
|
+
status: 'started',
|
|
936
|
+
remoteUrl: target.remoteUrl,
|
|
937
|
+
unitId: declaration.unitId,
|
|
832
938
|
});
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
939
|
+
const probeAction = {
|
|
940
|
+
actionType: ActionType.DISTRIBUTE_PROBE,
|
|
941
|
+
targetId: target.id,
|
|
942
|
+
remoteUrl: target.remoteUrl,
|
|
943
|
+
tag: declaration.tag,
|
|
944
|
+
};
|
|
945
|
+
let probeResult;
|
|
946
|
+
try {
|
|
947
|
+
probeResult = await probeAdapter.preflight(probeAction, {
|
|
948
|
+
externalWritesAuthorized: false,
|
|
949
|
+
plan,
|
|
950
|
+
root,
|
|
951
|
+
runDir,
|
|
952
|
+
});
|
|
953
|
+
} catch (err) {
|
|
954
|
+
probeResult = { status: 'PREFLIGHT_FAILED', error: err?.message ?? String(err), details: null };
|
|
955
|
+
}
|
|
956
|
+
probeCheckpoint.finishedAt = clockFn();
|
|
957
|
+
if (probeResult.status !== 'PREFLIGHT_PASSED') {
|
|
958
|
+
const code = [REMOTE_UNAVAILABLE, REMOTE_CONFLICT].includes(probeResult.details?.code)
|
|
959
|
+
? probeResult.details.code
|
|
960
|
+
: GATE_FAILED;
|
|
961
|
+
probeCheckpoint.status = 'FAILED';
|
|
962
|
+
probeCheckpoint.error = { code, message: probeResult.error ?? 'distribute probe failed' };
|
|
963
|
+
await evidence.append({
|
|
964
|
+
phase: 'safety-gate',
|
|
965
|
+
gate: `probe-${target.id}`,
|
|
966
|
+
status: 'failed',
|
|
967
|
+
error: asError('PROBE_FAILED', probeResult.error),
|
|
968
|
+
details: { code },
|
|
969
|
+
});
|
|
970
|
+
await recordFailure();
|
|
971
|
+
throw new ReleaseError(
|
|
972
|
+
code,
|
|
973
|
+
`distribute probe failed for target "${target.id}": ${probeResult.error ?? 'remote unreachable'}`,
|
|
974
|
+
{ targetId: target.id, remoteUrl: target.remoteUrl },
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
probeCheckpoint.status = 'SUCCEEDED';
|
|
978
|
+
probeObservations.set(target.id, probeResult.observation ?? {});
|
|
843
979
|
await evidence.append({
|
|
844
980
|
phase: 'safety-gate',
|
|
845
981
|
gate: `probe-${target.id}`,
|
|
846
|
-
status: '
|
|
847
|
-
|
|
848
|
-
|
|
982
|
+
status: 'passed',
|
|
983
|
+
tagExists: probeResult.observation?.tagExists ?? false,
|
|
984
|
+
unitId: declaration.unitId,
|
|
849
985
|
});
|
|
850
|
-
await recordBlocked();
|
|
851
|
-
throw new ReleaseError(
|
|
852
|
-
code,
|
|
853
|
-
`distribute probe failed for target "${target.id}": ${probeResult.error ?? 'remote unreachable'}`,
|
|
854
|
-
{ targetId: target.id, remoteUrl: target.remoteUrl },
|
|
855
|
-
);
|
|
856
986
|
}
|
|
857
|
-
|
|
858
|
-
probeObservations.set(target.id, probeResult.observation ?? {});
|
|
859
|
-
await evidence.append({
|
|
860
|
-
phase: 'safety-gate',
|
|
861
|
-
gate: `probe-${target.id}`,
|
|
862
|
-
status: 'passed',
|
|
863
|
-
tagExists: probeResult.observation?.tagExists ?? false,
|
|
864
|
-
});
|
|
987
|
+
prep.probeObservations = probeObservations;
|
|
865
988
|
}
|
|
866
989
|
|
|
867
990
|
// =======================================================================
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
991
|
+
// R-01 (rework): preset deterministic-conflict preflight — the narrow
|
|
992
|
+
// READ-ONLY entry (core/preset-executor.mjs preflightPresetHook) calls the
|
|
993
|
+
// SAME observation implementation the execution-phase delivery uses, for
|
|
994
|
+
// every preset hook that will actually execute this run. A deterministic
|
|
995
|
+
// conflict (e.g. the proposal path already exists with different bytes)
|
|
996
|
+
// fails the whole saga here, BEFORE the first external write. This is not
|
|
997
|
+
// a lock: the execution phase re-observes every remote regardless. Hooks
|
|
998
|
+
// parked at AWAITING_APPROVAL are not observed here — the approved
|
|
999
|
+
// reconcile rerun observes them at execution time.
|
|
872
1000
|
// =======================================================================
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1001
|
+
for (const prep of prepared) {
|
|
1002
|
+
for (const hook of prep.distributeHooks) {
|
|
1003
|
+
if (effectiveHookRequiresApproval(hook) && !approvedHookIds.has(hook.id)) continue;
|
|
1004
|
+
// Lineage-stable proposal identity: the SAME deterministic projection
|
|
1005
|
+
// the execution phase serializes (never the per-attempt runId).
|
|
1006
|
+
const proposalProjection = buildPostPublishContext({
|
|
1007
|
+
plan,
|
|
1008
|
+
postPublish: prep.declaration,
|
|
1009
|
+
runId: `distribute-${sourceRunId}`,
|
|
1010
|
+
sourceRun,
|
|
1011
|
+
payloadDir: undefined,
|
|
1012
|
+
phase: 'distribute',
|
|
1013
|
+
});
|
|
1014
|
+
const observation = await preflightPresetHook({
|
|
1015
|
+
hook,
|
|
1016
|
+
contextProjection: proposalProjection,
|
|
1017
|
+
exec,
|
|
1018
|
+
});
|
|
1019
|
+
if (observation !== null) {
|
|
1020
|
+
await evidence.append({
|
|
1021
|
+
phase: 'safety-gate',
|
|
1022
|
+
gate: 'preset-preflight',
|
|
1023
|
+
status: 'passed',
|
|
1024
|
+
hookId: hook.id,
|
|
1025
|
+
unitId: prep.declaration.unitId,
|
|
1026
|
+
verdict: observation.verdict,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
889
1030
|
}
|
|
890
|
-
await evidence.append({ phase: 'worktree', status: 'passed' });
|
|
891
1031
|
|
|
892
1032
|
// =======================================================================
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
1033
|
+
// Phase B — serial execution in frozen declaration order. Each declaration
|
|
1034
|
+
// gets its own detached tag worktree, execution-bundle install, payload
|
|
1035
|
+
// staging, steps, target writes and distribute-phase hooks. The saga keeps
|
|
1036
|
+
// ONE run chain: a failure stops all later checkpoints (targets and hooks
|
|
1037
|
+
// of later declarations), and the first external success followed by a
|
|
1038
|
+
// failure enters PARTIAL (success checkpoints are retained — never rolled
|
|
1039
|
+
// back). Legacy plans iterate exactly one declaration (normalize view)
|
|
1040
|
+
// with byte-identical behavior.
|
|
901
1041
|
// =======================================================================
|
|
1042
|
+
|
|
902
1043
|
// F-04 root split: the saga holds TWO distinct roots from here on —
|
|
903
1044
|
// - releaseWorkspaceRoot: the real project root the user releases from;
|
|
904
1045
|
// only used to resolve preset target.workspace, compare the release-
|
|
@@ -907,7 +1048,6 @@ export async function distributeRelease(options) {
|
|
|
907
1048
|
// frozen tagCommit; only used as the hook runner context.root for
|
|
908
1049
|
// materialize, steps and custom command hooks.
|
|
909
1050
|
// The two roots never fall back onto each other through defaults.
|
|
910
|
-
// =======================================================================
|
|
911
1051
|
let releaseWorkspaceRoot;
|
|
912
1052
|
try {
|
|
913
1053
|
releaseWorkspaceRoot = await realpath(root);
|
|
@@ -918,31 +1058,115 @@ export async function distributeRelease(options) {
|
|
|
918
1058
|
{ root, cause: err.code },
|
|
919
1059
|
));
|
|
920
1060
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1061
|
+
|
|
1062
|
+
const adapterContext = {
|
|
1063
|
+
externalWritesAuthorized: true, // dry-run side effects are adapter-guaranteed zero
|
|
1064
|
+
plan,
|
|
1065
|
+
root,
|
|
1066
|
+
runDir,
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
// Rework R-03: a stop propagates to the SAGA level — when a previous
|
|
1070
|
+
// declaration's distribute-phase preset or custom hook failed (timeout,
|
|
1071
|
+
// non-zero exit, execution failure), the remaining declaration's
|
|
1072
|
+
// materialize, steps, targets, presets and custom hooks must NOT execute;
|
|
1073
|
+
// its planned checkpoints are recorded SKIPPED.
|
|
1074
|
+
const skipReason = () => (stopReason === 'HOOK' ? 'EARLIER_HOOK_FAILED' : 'EARLIER_TARGET_FAILED');
|
|
1075
|
+
|
|
1076
|
+
for (const prep of prepared) {
|
|
1077
|
+
const {
|
|
1078
|
+
declaration,
|
|
1079
|
+
orderedTargets,
|
|
1080
|
+
distributeHooks,
|
|
1081
|
+
deferredPostVerifyHooks,
|
|
1082
|
+
probeObservations,
|
|
1083
|
+
tagAuthority,
|
|
1084
|
+
} = prep;
|
|
1085
|
+
|
|
1086
|
+
if (stopped) {
|
|
1087
|
+
for (const target of orderedTargets) {
|
|
1088
|
+
const cp = checkpointById.get(unitActionId(declaration.unitId, target.id));
|
|
1089
|
+
cp.status = 'SKIPPED';
|
|
1090
|
+
cp.reason = skipReason();
|
|
1091
|
+
cp.finishedAt = clockFn();
|
|
1092
|
+
}
|
|
1093
|
+
for (const hook of distributeHooks) {
|
|
1094
|
+
const cp = checkpointById.get(unitActionId(declaration.unitId, hook.id));
|
|
1095
|
+
cp.status = 'SKIPPED';
|
|
1096
|
+
cp.reason = skipReason();
|
|
1097
|
+
cp.finishedAt = clockFn();
|
|
1098
|
+
}
|
|
1099
|
+
await evidence.append({
|
|
1100
|
+
phase: 'postpublish-declaration',
|
|
1101
|
+
status: 'skipped',
|
|
1102
|
+
reason: skipReason(),
|
|
1103
|
+
unitId: declaration.unitId,
|
|
1104
|
+
});
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// R1 timing contract: detached worktree at the frozen tagCommit. The
|
|
1109
|
+
// payload may never come from the live workspace. R-01: the worktree is
|
|
1110
|
+
// materialized from a COPY of the frozen bare repo (never registered in
|
|
1111
|
+
// the source repo's worktree metadata, never touching its refs).
|
|
1112
|
+
await evidence.append({
|
|
1113
|
+
phase: 'worktree',
|
|
1114
|
+
status: 'started',
|
|
1115
|
+
tagCommit: tagAuthority.commit,
|
|
1116
|
+
unitId: declaration.unitId,
|
|
1117
|
+
});
|
|
1118
|
+
try {
|
|
1119
|
+
tmpBase = await mkdtemp(join(tmpdir(), 'release-skill-distribute-'));
|
|
1120
|
+
({ worktreePath } = await createFrozenTagWorktree({
|
|
1121
|
+
gitDir: tagAuthority.gitDir,
|
|
1122
|
+
commit: tagAuthority.commit,
|
|
1123
|
+
tmpBase,
|
|
1124
|
+
exec,
|
|
1125
|
+
}));
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
await evidence.append({ phase: 'worktree', status: 'failed', error: asError('WORKTREE_FAILED', boundedOutputTail(err?.stderr ?? err?.message)) });
|
|
1128
|
+
await failBlocked(new ReleaseError(
|
|
1129
|
+
GATE_FAILED,
|
|
1130
|
+
`cannot create the detached tag worktree at ${tagAuthority.commit}: ${err?.message ?? err}`,
|
|
1131
|
+
{ tagCommit: tagAuthority.commit },
|
|
1132
|
+
));
|
|
1133
|
+
}
|
|
1134
|
+
await evidence.append({ phase: 'worktree', status: 'passed' });
|
|
1135
|
+
|
|
1136
|
+
// =====================================================================
|
|
1137
|
+
// Private execution bundle (F-01 / T1): consumer-declared commands may
|
|
1138
|
+
// reference scripts that exist only in the parent workspace (tooling
|
|
1139
|
+
// never leaks into the frozen public surface). prepare froze those bytes
|
|
1140
|
+
// digest-addressed under the plan's .release-skill; re-verify them
|
|
1141
|
+
// through Foundation and install ONLY the verified bytes into the tag
|
|
1142
|
+
// worktree before any hook runs. Any mismatch fails closed here —
|
|
1143
|
+
// before any hook and before any external write.
|
|
1144
|
+
// =====================================================================
|
|
1145
|
+
let installedBundlePaths = [];
|
|
1146
|
+
try {
|
|
1147
|
+
({ installed: installedBundlePaths } = await verifyAndInstallExecutionBundle({
|
|
1148
|
+
planPath,
|
|
1149
|
+
worktreePath,
|
|
1150
|
+
postPublish: declaration,
|
|
1151
|
+
}));
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
await evidence.append({
|
|
1154
|
+
phase: 'worktree',
|
|
1155
|
+
gate: 'execution-bundle',
|
|
1156
|
+
status: 'failed',
|
|
1157
|
+
error: asError('EXECUTION_BUNDLE_FAILED', boundedOutputTail(err?.message ?? String(err))),
|
|
1158
|
+
});
|
|
1159
|
+
await failBlocked(err instanceof ReleaseError ? err : new ReleaseError(
|
|
1160
|
+
GATE_FAILED,
|
|
1161
|
+
`cannot verify the frozen execution bundle: ${err?.message ?? err}`,
|
|
1162
|
+
));
|
|
1163
|
+
}
|
|
929
1164
|
await evidence.append({
|
|
930
1165
|
phase: 'worktree',
|
|
931
1166
|
gate: 'execution-bundle',
|
|
932
|
-
status: '
|
|
933
|
-
|
|
1167
|
+
status: 'passed',
|
|
1168
|
+
installed: installedBundlePaths,
|
|
934
1169
|
});
|
|
935
|
-
await failBlocked(err instanceof ReleaseError ? err : new ReleaseError(
|
|
936
|
-
GATE_FAILED,
|
|
937
|
-
`cannot verify the frozen execution bundle: ${err?.message ?? err}`,
|
|
938
|
-
));
|
|
939
|
-
}
|
|
940
|
-
await evidence.append({
|
|
941
|
-
phase: 'worktree',
|
|
942
|
-
gate: 'execution-bundle',
|
|
943
|
-
status: 'passed',
|
|
944
|
-
installed: installedBundlePaths,
|
|
945
|
-
});
|
|
946
1170
|
|
|
947
1171
|
// =======================================================================
|
|
948
1172
|
// Payload staging (fail-closed). The frozen plan selects exactly one
|
|
@@ -959,7 +1183,7 @@ export async function distributeRelease(options) {
|
|
|
959
1183
|
// rollback belongs to Foundation and is never reimplemented here.
|
|
960
1184
|
// =======================================================================
|
|
961
1185
|
let payloadReal;
|
|
962
|
-
const materialize =
|
|
1186
|
+
const materialize = declaration.materialize;
|
|
963
1187
|
|
|
964
1188
|
if (materialize !== undefined) {
|
|
965
1189
|
await evidence.append({ phase: 'materialize', status: 'started' });
|
|
@@ -1058,7 +1282,7 @@ export async function distributeRelease(options) {
|
|
|
1058
1282
|
|
|
1059
1283
|
// Release-domain parameter selection ONLY: the frozen mapping rides the
|
|
1060
1284
|
// plan digest; live project configuration is never read here.
|
|
1061
|
-
const publicFiles =
|
|
1285
|
+
const publicFiles = declaration.executionBundle?.publicFiles;
|
|
1062
1286
|
if (!Array.isArray(publicFiles) || publicFiles.length === 0) {
|
|
1063
1287
|
await evidence.append({
|
|
1064
1288
|
phase: 'materialize',
|
|
@@ -1107,7 +1331,7 @@ export async function distributeRelease(options) {
|
|
|
1107
1331
|
// =======================================================================
|
|
1108
1332
|
// Declared postPublish steps, in order (fail-closed).
|
|
1109
1333
|
// =======================================================================
|
|
1110
|
-
for (const step of
|
|
1334
|
+
for (const step of declaration.steps ?? []) {
|
|
1111
1335
|
if (dryRun === true) {
|
|
1112
1336
|
// R1 dry-run contract: steps are arbitrary project code with
|
|
1113
1337
|
// potentially remote side effects; they never execute in a rehearsal.
|
|
@@ -1161,42 +1385,33 @@ export async function distributeRelease(options) {
|
|
|
1161
1385
|
}
|
|
1162
1386
|
|
|
1163
1387
|
// =======================================================================
|
|
1164
|
-
// Target distribution: sequential, dependency-ordered,
|
|
1165
|
-
// write (reconcile equivalence — never force).
|
|
1388
|
+
// Target distribution (per declaration): sequential, dependency-ordered,
|
|
1389
|
+
// observe-before-write (reconcile equivalence — never force).
|
|
1166
1390
|
// =======================================================================
|
|
1167
1391
|
await evidence.append({
|
|
1168
1392
|
phase: 'distribute',
|
|
1169
1393
|
status: 'started',
|
|
1394
|
+
unitId: declaration.unitId,
|
|
1170
1395
|
targetCount: orderedTargets.length,
|
|
1171
1396
|
dryRun: dryRun === true,
|
|
1172
1397
|
});
|
|
1173
1398
|
|
|
1174
|
-
const mirrorResults = new Map(); // targetId -> { sha } for dependents
|
|
1175
|
-
let stopped = false;
|
|
1176
|
-
let failures = 0;
|
|
1177
|
-
let pushedWrites = 0;
|
|
1178
|
-
|
|
1179
|
-
const adapterContext = {
|
|
1180
|
-
externalWritesAuthorized: true, // dry-run side effects are adapter-guaranteed zero
|
|
1181
|
-
plan,
|
|
1182
|
-
root,
|
|
1183
|
-
runDir,
|
|
1184
|
-
};
|
|
1399
|
+
const mirrorResults = new Map(); // targetId -> { sha } for dependents (declaration-local)
|
|
1185
1400
|
|
|
1186
1401
|
for (const target of orderedTargets) {
|
|
1187
|
-
const cp = checkpointById.get(target.id);
|
|
1402
|
+
const cp = checkpointById.get(unitActionId(declaration.unitId, target.id));
|
|
1188
1403
|
cp.startedAt = clockFn();
|
|
1189
1404
|
|
|
1190
1405
|
if (stopped) {
|
|
1191
1406
|
cp.status = 'SKIPPED';
|
|
1192
1407
|
cp.reason = 'EARLIER_TARGET_FAILED';
|
|
1193
1408
|
cp.finishedAt = clockFn();
|
|
1194
|
-
await evidence.append({ phase: 'checkpoint', actionId: target.id, status: 'skipped', reason: 'EARLIER_TARGET_FAILED' });
|
|
1409
|
+
await evidence.append({ phase: 'checkpoint', actionId: unitActionId(declaration.unitId, target.id), status: 'skipped', reason: 'EARLIER_TARGET_FAILED' });
|
|
1195
1410
|
continue;
|
|
1196
1411
|
}
|
|
1197
1412
|
|
|
1198
1413
|
// --- Pre-observe: a remote already at the frozen tag is SKIPPED. ---
|
|
1199
|
-
await evidence.append({ phase: 'checkpoint', actionId: target.id, status: 'pre-observe' });
|
|
1414
|
+
await evidence.append({ phase: 'checkpoint', actionId: unitActionId(declaration.unitId, target.id), status: 'pre-observe' });
|
|
1200
1415
|
let preObserved = {};
|
|
1201
1416
|
try {
|
|
1202
1417
|
const observeResult = await mirrorAdapter.observe(
|
|
@@ -1205,7 +1420,7 @@ export async function distributeRelease(options) {
|
|
|
1205
1420
|
targetId: target.id,
|
|
1206
1421
|
remoteUrl: target.remoteUrl,
|
|
1207
1422
|
branch: target.branch,
|
|
1208
|
-
tag:
|
|
1423
|
+
tag: declaration.tag,
|
|
1209
1424
|
},
|
|
1210
1425
|
adapterContext,
|
|
1211
1426
|
);
|
|
@@ -1214,7 +1429,7 @@ export async function distributeRelease(options) {
|
|
|
1214
1429
|
preObserved = {};
|
|
1215
1430
|
await evidence.append({
|
|
1216
1431
|
phase: 'checkpoint',
|
|
1217
|
-
actionId: target.id,
|
|
1432
|
+
actionId: unitActionId(declaration.unitId, target.id),
|
|
1218
1433
|
status: 'pre-observe-unobservable',
|
|
1219
1434
|
error: asError('PRE_OBSERVE_FAILED', err?.message ?? String(err)),
|
|
1220
1435
|
});
|
|
@@ -1226,7 +1441,7 @@ export async function distributeRelease(options) {
|
|
|
1226
1441
|
mirrorResults.set(target.id, { sha: preObserved.tagOid });
|
|
1227
1442
|
await evidence.append({
|
|
1228
1443
|
phase: 'checkpoint',
|
|
1229
|
-
actionId: target.id,
|
|
1444
|
+
actionId: unitActionId(declaration.unitId, target.id),
|
|
1230
1445
|
status: 'skipped',
|
|
1231
1446
|
details: { preObserve: 'CONSISTENT', sha: preObserved.tagOid },
|
|
1232
1447
|
});
|
|
@@ -1244,15 +1459,15 @@ export async function distributeRelease(options) {
|
|
|
1244
1459
|
kind: target.kind,
|
|
1245
1460
|
remoteUrl: target.remoteUrl,
|
|
1246
1461
|
branch: target.branch,
|
|
1247
|
-
tag:
|
|
1248
|
-
commitIdentity:
|
|
1462
|
+
tag: declaration.tag,
|
|
1463
|
+
commitIdentity: declaration.commitIdentity,
|
|
1249
1464
|
dryRun: dryRun === true,
|
|
1250
1465
|
};
|
|
1251
1466
|
if (target.kind === 'payload-mirror') {
|
|
1252
1467
|
action.payloadDir = payloadReal;
|
|
1253
1468
|
} else {
|
|
1254
1469
|
action.marketplace = target.marketplace;
|
|
1255
|
-
action.pluginName = resolvePluginName(plan,
|
|
1470
|
+
action.pluginName = resolvePluginName(plan, declaration.unitId);
|
|
1256
1471
|
const dependencyTarget = orderedTargets.find((entry) => entry.id === target.dependsOn);
|
|
1257
1472
|
action.dependency = {
|
|
1258
1473
|
remoteUrl: dependencyTarget.remoteUrl,
|
|
@@ -1274,7 +1489,7 @@ export async function distributeRelease(options) {
|
|
|
1274
1489
|
});
|
|
1275
1490
|
|
|
1276
1491
|
// --- Execute (the adapter never throws; it returns EXECUTE_FAILED). ---
|
|
1277
|
-
await evidence.append({ phase: 'checkpoint', actionId: target.id, status: 'started' });
|
|
1492
|
+
await evidence.append({ phase: 'checkpoint', actionId: unitActionId(declaration.unitId, target.id), status: 'started' });
|
|
1278
1493
|
let executeResult;
|
|
1279
1494
|
try {
|
|
1280
1495
|
executeResult = await mirrorAdapter.execute(action, adapterContext);
|
|
@@ -1295,9 +1510,10 @@ export async function distributeRelease(options) {
|
|
|
1295
1510
|
cp.finishedAt = clockFn();
|
|
1296
1511
|
failures += 1;
|
|
1297
1512
|
stopped = true;
|
|
1513
|
+
stopReason = 'TARGET';
|
|
1298
1514
|
await evidence.append({
|
|
1299
1515
|
phase: 'checkpoint',
|
|
1300
|
-
actionId: target.id,
|
|
1516
|
+
actionId: unitActionId(declaration.unitId, target.id),
|
|
1301
1517
|
status: 'failed',
|
|
1302
1518
|
error: asError('EXECUTE_FAILED', executeResult.error),
|
|
1303
1519
|
details: { code },
|
|
@@ -1317,7 +1533,7 @@ export async function distributeRelease(options) {
|
|
|
1317
1533
|
?? observation.previousHead
|
|
1318
1534
|
?? null;
|
|
1319
1535
|
mirrorResults.set(target.id, { sha });
|
|
1320
|
-
await evidence.append({ phase: 'checkpoint', actionId: target.id, status: 'no-change' });
|
|
1536
|
+
await evidence.append({ phase: 'checkpoint', actionId: unitActionId(declaration.unitId, target.id), status: 'no-change' });
|
|
1321
1537
|
await snapshot(PARTIAL);
|
|
1322
1538
|
continue;
|
|
1323
1539
|
}
|
|
@@ -1345,7 +1561,7 @@ export async function distributeRelease(options) {
|
|
|
1345
1561
|
targetId: target.id,
|
|
1346
1562
|
remoteUrl: target.remoteUrl,
|
|
1347
1563
|
branch: target.branch,
|
|
1348
|
-
tag:
|
|
1564
|
+
tag: declaration.tag,
|
|
1349
1565
|
...(pushed ? { pushedCommit: observation.pushedCommit } : {}),
|
|
1350
1566
|
},
|
|
1351
1567
|
adapterContext,
|
|
@@ -1364,9 +1580,10 @@ export async function distributeRelease(options) {
|
|
|
1364
1580
|
cp.finishedAt = clockFn();
|
|
1365
1581
|
failures += 1;
|
|
1366
1582
|
stopped = true;
|
|
1583
|
+
stopReason = 'TARGET';
|
|
1367
1584
|
await evidence.append({
|
|
1368
1585
|
phase: 'checkpoint',
|
|
1369
|
-
actionId: target.id,
|
|
1586
|
+
actionId: unitActionId(declaration.unitId, target.id),
|
|
1370
1587
|
status: 'verify-failed',
|
|
1371
1588
|
error: asError('POST_PUBLISH_VERIFY_FAILED', verifyResult.error ?? null),
|
|
1372
1589
|
});
|
|
@@ -1379,7 +1596,7 @@ export async function distributeRelease(options) {
|
|
|
1379
1596
|
cp.finishedAt = clockFn();
|
|
1380
1597
|
await evidence.append({
|
|
1381
1598
|
phase: 'checkpoint',
|
|
1382
|
-
actionId: target.id,
|
|
1599
|
+
actionId: unitActionId(declaration.unitId, target.id),
|
|
1383
1600
|
status: 'completed',
|
|
1384
1601
|
mode: observation.mode,
|
|
1385
1602
|
pushedCommit: pushed ? observation.pushedCommit : null,
|
|
@@ -1397,14 +1614,22 @@ export async function distributeRelease(options) {
|
|
|
1397
1614
|
// the independent postVerify run (R3) and are evidenced as deferred.
|
|
1398
1615
|
// =======================================================================
|
|
1399
1616
|
if (deferredPostVerifyHooks > 0) {
|
|
1400
|
-
await evidence.append({
|
|
1617
|
+
await evidence.append({
|
|
1618
|
+
phase: 'postpublish-hooks',
|
|
1619
|
+
status: 'deferred',
|
|
1620
|
+
deferredPostVerifyHooks,
|
|
1621
|
+
unitId: declaration.unitId,
|
|
1622
|
+
});
|
|
1401
1623
|
}
|
|
1402
1624
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1625
|
+
// Per-declaration hook chain: a failure inside this declaration stops the
|
|
1626
|
+
// SAGA (rework R-03) — the remaining hooks of this declaration and every
|
|
1627
|
+
// later declaration's materialize, steps, targets, presets and custom
|
|
1628
|
+
// hooks are SKIPPED. `stopped`/`stopReason` are the single saga-level
|
|
1629
|
+
// chain state.
|
|
1406
1630
|
const hookContextProjection = buildPostPublishContext({
|
|
1407
1631
|
plan,
|
|
1632
|
+
postPublish: declaration,
|
|
1408
1633
|
runId,
|
|
1409
1634
|
sourceRun,
|
|
1410
1635
|
payloadDir: payloadReal,
|
|
@@ -1421,18 +1646,18 @@ export async function distributeRelease(options) {
|
|
|
1421
1646
|
};
|
|
1422
1647
|
|
|
1423
1648
|
for (const hook of distributeHooks) {
|
|
1424
|
-
const cp = checkpointById.get(hook.id);
|
|
1649
|
+
const cp = checkpointById.get(unitActionId(declaration.unitId, hook.id));
|
|
1425
1650
|
cp.startedAt = clockFn();
|
|
1426
1651
|
|
|
1427
|
-
if (
|
|
1652
|
+
if (stopped) {
|
|
1428
1653
|
cp.status = 'SKIPPED';
|
|
1429
|
-
cp.reason =
|
|
1654
|
+
cp.reason = skipReason();
|
|
1430
1655
|
cp.finishedAt = clockFn();
|
|
1431
1656
|
await evidence.append({
|
|
1432
1657
|
phase: 'postpublish-hook',
|
|
1433
1658
|
hookId: hook.id,
|
|
1434
1659
|
status: 'skipped',
|
|
1435
|
-
reason:
|
|
1660
|
+
reason: skipReason(),
|
|
1436
1661
|
});
|
|
1437
1662
|
continue;
|
|
1438
1663
|
}
|
|
@@ -1468,7 +1693,7 @@ export async function distributeRelease(options) {
|
|
|
1468
1693
|
hook,
|
|
1469
1694
|
contextProjection: hookContextProjection,
|
|
1470
1695
|
proposalContextProjection,
|
|
1471
|
-
commitIdentity:
|
|
1696
|
+
commitIdentity: declaration.commitIdentity,
|
|
1472
1697
|
// F-04: presets receive the RELEASE workspace root (target.workspace
|
|
1473
1698
|
// resolution + release-workspace write exclusion). The detached
|
|
1474
1699
|
// worktree is the execution worktree and never impersonates it.
|
|
@@ -1484,7 +1709,8 @@ export async function distributeRelease(options) {
|
|
|
1484
1709
|
cp.error = { code, message: err?.message ?? String(err) };
|
|
1485
1710
|
cp.finishedAt = clockFn();
|
|
1486
1711
|
failures += 1;
|
|
1487
|
-
|
|
1712
|
+
stopped = true;
|
|
1713
|
+
stopReason = 'HOOK';
|
|
1488
1714
|
await evidence.append({
|
|
1489
1715
|
phase: 'postpublish-hook',
|
|
1490
1716
|
hookId: hook.id,
|
|
@@ -1555,7 +1781,8 @@ export async function distributeRelease(options) {
|
|
|
1555
1781
|
};
|
|
1556
1782
|
cp.finishedAt = clockFn();
|
|
1557
1783
|
failures += 1;
|
|
1558
|
-
|
|
1784
|
+
stopped = true;
|
|
1785
|
+
stopReason = 'HOOK';
|
|
1559
1786
|
await evidence.append({
|
|
1560
1787
|
phase: 'postpublish-hook',
|
|
1561
1788
|
hookId: hook.id,
|
|
@@ -1571,6 +1798,7 @@ export async function distributeRelease(options) {
|
|
|
1571
1798
|
try {
|
|
1572
1799
|
hookExecution = await hookRunner(
|
|
1573
1800
|
{
|
|
1801
|
+
id: hook.id,
|
|
1574
1802
|
command: hook.command,
|
|
1575
1803
|
...(hook.cwd ? { cwd: hook.cwd } : {}),
|
|
1576
1804
|
...(hook.timeoutMs !== undefined ? { timeoutMs: hook.timeoutMs } : {}),
|
|
@@ -1593,7 +1821,8 @@ export async function distributeRelease(options) {
|
|
|
1593
1821
|
cp.error = { code, message: err?.message ?? String(err) };
|
|
1594
1822
|
cp.finishedAt = clockFn();
|
|
1595
1823
|
failures += 1;
|
|
1596
|
-
|
|
1824
|
+
stopped = true;
|
|
1825
|
+
stopReason = 'HOOK';
|
|
1597
1826
|
await evidence.append({
|
|
1598
1827
|
phase: 'postpublish-hook',
|
|
1599
1828
|
hookId: hook.id,
|
|
@@ -1612,7 +1841,8 @@ export async function distributeRelease(options) {
|
|
|
1612
1841
|
};
|
|
1613
1842
|
cp.finishedAt = clockFn();
|
|
1614
1843
|
failures += 1;
|
|
1615
|
-
|
|
1844
|
+
stopped = true;
|
|
1845
|
+
stopReason = 'HOOK';
|
|
1616
1846
|
await evidence.append({
|
|
1617
1847
|
phase: 'postpublish-hook',
|
|
1618
1848
|
hookId: hook.id,
|
|
@@ -1632,6 +1862,11 @@ export async function distributeRelease(options) {
|
|
|
1632
1862
|
await snapshot(PARTIAL);
|
|
1633
1863
|
}
|
|
1634
1864
|
|
|
1865
|
+
// This declaration's scratch worktree is consumed; release it. The
|
|
1866
|
+
// outer finally keeps the safety net for a mid-declaration throw.
|
|
1867
|
+
await cleanupWorktree();
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1635
1870
|
// =======================================================================
|
|
1636
1871
|
// Classification (returned, not thrown):
|
|
1637
1872
|
// - DISTRIBUTED: no failures and no awaiting-approval hooks;
|
|
@@ -1675,14 +1910,20 @@ export async function distributeRelease(options) {
|
|
|
1675
1910
|
} catch (err) {
|
|
1676
1911
|
// The happy path returns before this catch, so `evidence.finish` can never
|
|
1677
1912
|
// double-run here: it must ALWAYS close the evidence stream and seal the
|
|
1678
|
-
// summary, even when failBlocked already persisted the
|
|
1913
|
+
// summary, even when failBlocked already persisted the failure record.
|
|
1914
|
+
// Rework R-01: the persisted status and the summary reflect the existing
|
|
1915
|
+
// success checkpoints — PARTIAL once one external checkpoint succeeded,
|
|
1916
|
+
// BLOCKED at zero external side effects.
|
|
1917
|
+
const catchStatus = !lineageKnown
|
|
1918
|
+
? 'FAILED'
|
|
1919
|
+
: (pushedWrites + hookSuccesses) > 0 ? PARTIAL : BLOCKED;
|
|
1679
1920
|
try {
|
|
1680
1921
|
await evidence.append({
|
|
1681
1922
|
phase: 'distribute',
|
|
1682
1923
|
status: 'failed',
|
|
1683
1924
|
error: { code: err.code, message: err.message },
|
|
1684
1925
|
});
|
|
1685
|
-
if (lineageKnown && !finalRecordWritten) await
|
|
1926
|
+
if (lineageKnown && !finalRecordWritten) await recordFailure();
|
|
1686
1927
|
} catch {
|
|
1687
1928
|
// Persistence must never mask the primary failure.
|
|
1688
1929
|
}
|
|
@@ -1691,7 +1932,7 @@ export async function distributeRelease(options) {
|
|
|
1691
1932
|
});
|
|
1692
1933
|
err.details = { ...err.details, recoveryActionCode };
|
|
1693
1934
|
await evidence.finish({
|
|
1694
|
-
status:
|
|
1935
|
+
status: catchStatus,
|
|
1695
1936
|
recoveryActionCode,
|
|
1696
1937
|
error: { code: err.code, message: err.message, details: err.details },
|
|
1697
1938
|
details: { recoveryActionCode },
|