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.
Files changed (144) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +65 -7
  7. package/INSTALL.md +14 -2
  8. package/INSTALL.zh-CN.md +12 -2
  9. package/README.md +51 -31
  10. package/README.zh-CN.md +33 -20
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/error-codes.json +1 -1
  14. package/adapters/claude/bin/foundation-resource-binding.json +1 -1
  15. package/adapters/claude/bin/prebuild-manifest.json +8 -4
  16. package/adapters/claude/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  17. package/adapters/claude/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  18. package/adapters/claude/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  19. package/adapters/claude/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  20. package/adapters/claude/bin/registry.json +22 -1
  21. package/adapters/claude/bin/release-skill-local-finish.mjs +4 -0
  22. package/adapters/claude/bin/release-skill.bundle.mjs +7152 -4231
  23. package/adapters/claude/bin/rules.json +1 -1
  24. package/adapters/claude/schemas/release-plan.schema.json +43 -5
  25. package/adapters/claude/schemas/release-run.schema.json +35 -1
  26. package/adapters/claude/skills/release-finish/SKILL.md +65 -0
  27. package/adapters/claude/skills/release-verify/SKILL.md +6 -1
  28. package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +55 -0
  29. package/adapters/claude/src/schemas/host-verification-result.schema.json +2 -2
  30. package/adapters/claude/src/schemas/plugin-verification-request.schema.json +374 -0
  31. package/adapters/claude/src/schemas/plugin-verification-result.schema.json +799 -0
  32. package/adapters/claude/src/schemas/watchdog-termination-envelope.schema.json +16 -0
  33. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  34. package/adapters/codex/bin/error-codes.json +1 -1
  35. package/adapters/codex/bin/foundation-resource-binding.json +1 -1
  36. package/adapters/codex/bin/prebuild-manifest.json +8 -4
  37. package/adapters/codex/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  38. package/adapters/codex/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  39. package/adapters/codex/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  40. package/adapters/codex/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  41. package/adapters/codex/bin/registry.json +22 -1
  42. package/adapters/codex/bin/release-skill-local-finish.mjs +4 -0
  43. package/adapters/codex/bin/release-skill.bundle.mjs +7152 -4231
  44. package/adapters/codex/bin/rules.json +1 -1
  45. package/adapters/codex/schemas/release-plan.schema.json +43 -5
  46. package/adapters/codex/schemas/release-run.schema.json +35 -1
  47. package/adapters/codex/skills/release-finish/SKILL.md +72 -0
  48. package/adapters/codex/skills/release-verify/SKILL.md +6 -1
  49. package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +55 -0
  50. package/adapters/codex/src/schemas/host-verification-result.schema.json +2 -2
  51. package/adapters/codex/src/schemas/plugin-verification-request.schema.json +374 -0
  52. package/adapters/codex/src/schemas/plugin-verification-result.schema.json +799 -0
  53. package/adapters/codex/src/schemas/watchdog-termination-envelope.schema.json +16 -0
  54. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  55. package/adapters/kimi/bin/error-codes.json +1 -1
  56. package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
  57. package/adapters/kimi/bin/prebuild-manifest.json +8 -4
  58. package/adapters/kimi/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  59. package/adapters/kimi/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  60. package/adapters/kimi/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  61. package/adapters/kimi/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  62. package/adapters/kimi/bin/registry.json +22 -1
  63. package/adapters/kimi/bin/release-skill-local-finish.mjs +4 -0
  64. package/adapters/kimi/bin/release-skill.bundle.mjs +7152 -4231
  65. package/adapters/kimi/bin/rules.json +1 -1
  66. package/adapters/kimi/schemas/release-plan.schema.json +43 -5
  67. package/adapters/kimi/schemas/release-run.schema.json +35 -1
  68. package/adapters/kimi/skills/release-finish/SKILL.md +72 -0
  69. package/adapters/kimi/skills/release-verify/SKILL.md +6 -1
  70. package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +55 -0
  71. package/adapters/kimi/src/schemas/host-verification-result.schema.json +2 -2
  72. package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +374 -0
  73. package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +799 -0
  74. package/adapters/kimi/src/schemas/watchdog-termination-envelope.schema.json +16 -0
  75. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  76. package/adapters/workbuddy/bin/error-codes.json +1 -1
  77. package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
  78. package/adapters/workbuddy/bin/prebuild-manifest.json +8 -4
  79. package/adapters/workbuddy/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  80. package/adapters/workbuddy/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  81. package/adapters/workbuddy/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  82. package/adapters/workbuddy/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  83. package/adapters/workbuddy/bin/registry.json +22 -1
  84. package/adapters/workbuddy/bin/release-skill-local-finish.mjs +4 -0
  85. package/adapters/workbuddy/bin/release-skill.bundle.mjs +7152 -4231
  86. package/adapters/workbuddy/bin/rules.json +1 -1
  87. package/adapters/workbuddy/schemas/release-plan.schema.json +43 -5
  88. package/adapters/workbuddy/schemas/release-run.schema.json +35 -1
  89. package/adapters/workbuddy/skills/release-finish/SKILL.md +65 -0
  90. package/adapters/workbuddy/skills/release-verify/SKILL.md +6 -1
  91. package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +55 -0
  92. package/adapters/workbuddy/src/schemas/host-verification-result.schema.json +2 -2
  93. package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +374 -0
  94. package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +799 -0
  95. package/adapters/workbuddy/src/schemas/watchdog-termination-envelope.schema.json +16 -0
  96. package/bin/error-codes.json +1 -1
  97. package/bin/foundation-resource-binding.json +1 -1
  98. package/bin/prebuild-manifest.json +8 -4
  99. package/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
  100. package/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
  101. package/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
  102. package/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
  103. package/bin/registry.json +22 -1
  104. package/bin/release-skill-cli.mjs +126 -1
  105. package/bin/release-skill-local-finish.mjs +4 -0
  106. package/bin/release-skill.bundle.mjs +7152 -4231
  107. package/bin/rules.json +1 -1
  108. package/package.json +4 -4
  109. package/platform-manifest.json +30 -4
  110. package/schemas/release-plan.schema.json +43 -5
  111. package/schemas/release-run.schema.json +35 -1
  112. package/skills/release-finish/SKILL.md +65 -0
  113. package/skills/release-verify/SKILL.md +6 -1
  114. package/skills-src/release-finish/SKILL.md +65 -0
  115. package/skills-src/release-verify/SKILL.md +6 -1
  116. package/src/commands/approve.mjs +14 -2
  117. package/src/commands/distribute.mjs +562 -321
  118. package/src/commands/post-release-local.mjs +611 -0
  119. package/src/commands/postverify.mjs +262 -155
  120. package/src/commands/prepare.mjs +83 -57
  121. package/src/commands/publish.mjs +11 -10
  122. package/src/commands/reconcile.mjs +11 -10
  123. package/src/commands/setup.mjs +26 -13
  124. package/src/commands/ship.mjs +45 -12
  125. package/src/commands/verify.mjs +81 -18
  126. package/src/core/adoption-assessment.mjs +0 -3
  127. package/src/core/approval.mjs +11 -8
  128. package/src/core/foundation-plugin-verification.mjs +247 -0
  129. package/src/core/hooks.mjs +0 -16
  130. package/src/core/plan.mjs +19 -11
  131. package/src/core/postpublish-approval.mjs +12 -2
  132. package/src/core/postpublish-bundle.mjs +51 -13
  133. package/src/core/postpublish.mjs +146 -6
  134. package/src/core/preset-executor.mjs +53 -0
  135. package/src/core/proposal-inbox.mjs +91 -27
  136. package/src/core/recovery.mjs +27 -12
  137. package/src/core/skill-resource-closure.mjs +8 -4
  138. package/src/producers/build-adapters.mjs +10 -1
  139. package/src/producers/foundation-resource-projection.mjs +3 -3
  140. package/src/schemas/filesystem-tree-observation.schema.json +55 -0
  141. package/src/schemas/host-verification-result.schema.json +2 -2
  142. package/src/schemas/plugin-verification-request.schema.json +374 -0
  143. package/src/schemas/plugin-verification-result.schema.json +799 -0
  144. package/src/schemas/watchdog-termination-envelope.schema.json +16 -0
@@ -58,13 +58,16 @@ import { boundedOutputTail } from '../core/bounded-output.mjs';
58
58
  import {
59
59
  buildPostPublishContext,
60
60
  validatePostPublishDeclaration,
61
+ normalizePostPublishView,
62
+ postPublishActionId,
61
63
  effectiveHookRequiresApproval,
64
+ validatePostPublishHookIdUniqueness,
62
65
  POSTPUBLISH_CONTEXT_ENV,
63
66
  PAYLOAD_SOURCE_TAG_WORKTREE,
64
67
  } from '../core/postpublish.mjs';
65
- import { verifyAndInstallExecutionBundle } from '../core/postpublish-bundle.mjs';
68
+ import { verifyAndInstallExecutionBundle, verifyExecutionBundle } from '../core/postpublish-bundle.mjs';
66
69
  import { assertPostPublishApprovalAuthority, validatePostPublishApproval } from '../core/postpublish-approval.mjs';
67
- import { executePresetHook } from '../core/preset-executor.mjs';
70
+ import { executePresetHook, preflightPresetHook } from '../core/preset-executor.mjs';
68
71
  import {
69
72
  ReleaseError,
70
73
  GATE_FAILED,
@@ -185,13 +188,28 @@ export async function postVerifyRelease(options) {
185
188
  { expected: plan.digest, actual: actualDigest },
186
189
  );
187
190
  }
188
- if (!plan.postPublish) {
191
+ // v3 contract (§4.3): postPublish is a frozen declaration ARRAY. All reads
192
+ // go through the single read-only normalization entry normalizePostPublishView.
193
+ // Legacy (planVersion 1/2) absent stays fail-closed; a v3 empty array means
194
+ // "no post-release execution" and completes directly with zero hooks.
195
+ const declarations = normalizePostPublishView(plan);
196
+ // Rework R-02: the single array-level explicit-hook-id uniqueness authority
197
+ // (core/postpublish.mjs) re-asserts the frozen view here — a digest-correct,
198
+ // schema-valid plan with duplicate hook ids across units must fail before
199
+ // any approval consumption or hook execution.
200
+ validatePostPublishHookIdUniqueness(declarations);
201
+ if (declarations.length === 0 && plan.planVersion !== 3) {
189
202
  throw new ReleaseError(
190
203
  GATE_FAILED,
191
204
  'release plan has no postPublish declaration; nothing to postVerify',
192
205
  { planPath },
193
206
  );
194
207
  }
208
+ if (declarations.length === 0) {
209
+ // v3 zero declarations (§6 matrix row 0): the post-release phase is
210
+ // skipped — no hook, no external write.
211
+ return { planPath, runPath: null, status: DISTRIBUTED, checkpoints: [] };
212
+ }
195
213
 
196
214
  // =========================================================================
197
215
  // Gate 2: approval record validation (binding checks; the post-publish
@@ -278,12 +296,29 @@ export async function postVerifyRelease(options) {
278
296
  validateRunCheckpointMapping(publishRun, plan.externalActions ?? []);
279
297
 
280
298
  // =========================================================================
281
- // Declaration re-validation + postVerify-phase hook selection.
299
+ // Declaration re-validation + postVerify-phase hook selection (v3 contract
300
+ // §4.3): EVERY declaration is validated before any write. Legacy plans
301
+ // present exactly one declaration (normalizePostPublishView) and keep their
302
+ // exact behavior.
282
303
  // =========================================================================
283
- const postPublish = plan.postPublish;
284
- validatePostPublishDeclaration(postPublish, { unitId: postPublish.unitId });
285
- const declaredHooks = postPublish.hooks ?? [];
286
- const postVerifyHooks = declaredHooks.filter((hook) => (hook.phase ?? 'distribute') === 'postVerify');
304
+ const v3 = plan.planVersion === 3;
305
+ // Checkpoint action ids are unit-namespaced on v3; legacy ids are unchanged
306
+ // (existing runs, tests and approvals pin the bare local ids). Shared
307
+ // derivation core/postpublish.mjs `postPublishActionId`.
308
+ const unitActionId = (unitId, localId) => postPublishActionId({ planVersion: plan.planVersion, unitId, localId });
309
+
310
+ const prepared = [];
311
+ for (const declaration of declarations) {
312
+ validatePostPublishDeclaration(declaration, { unitId: declaration.unitId });
313
+ // R-01 (rework): the frozen execution bundle closure is re-verified for
314
+ // EVERY declaration BEFORE any hook runs — a later declaration's
315
+ // missing/drifted bundle must fail the whole run before unit A executes.
316
+ await verifyExecutionBundle({ planPath, postPublish: declaration });
317
+ const declaredHooks = declaration.hooks ?? [];
318
+ const postVerifyHooks = declaredHooks.filter((hook) => (hook.phase ?? 'distribute') === 'postVerify');
319
+ prepared.push({ declaration, postVerifyHooks });
320
+ }
321
+ const postVerifyHooks = prepared.flatMap((prep) => prep.postVerifyHooks);
287
322
 
288
323
  // =========================================================================
289
324
  // Gate 4: checkpoint approvals for requiresApproval hooks. Every provided
@@ -341,50 +376,55 @@ export async function postVerifyRelease(options) {
341
376
  // authority; the live local tag is diagnostic only. Read-only observation,
342
377
  // still before any write.
343
378
  // =========================================================================
344
- if (postPublish.payloadSource !== PAYLOAD_SOURCE_TAG_WORKTREE) {
345
- throw new ReleaseError(
346
- GATE_FAILED,
347
- `postPublish.payloadSource must be "${PAYLOAD_SOURCE_TAG_WORKTREE}"`,
348
- { payloadSource: postPublish.payloadSource },
349
- );
350
- }
351
- if (typeof postPublish.tagCommit !== 'string' || !SHA_RE.test(postPublish.tagCommit)) {
352
- throw new ReleaseError(
353
- GATE_FAILED,
354
- 'postPublish is missing a frozen tagCommit binding; postVerify fails closed',
355
- { tagCommit: postPublish.tagCommit ?? null },
356
- );
357
- }
358
- let tagAuthority;
359
- try {
360
- tagAuthority = await resolveFrozenTagAuthority({
361
- plan,
362
- unitId: postPublish.unitId,
363
- // T04 (裁决 14): the tag proof is taken from the legitimate PUBLISHED
364
- // parent run — never from the verify receipt.
365
- sourceRun: publishRun,
366
- root,
367
- exec,
368
- observeTagFn,
369
- observeBranchFn,
370
- });
371
- } catch (err) {
372
- throw err instanceof ReleaseError ? err : new ReleaseError(
373
- GATE_FAILED,
374
- `tag identity cannot be verified: ${err?.message ?? err}`,
375
- );
376
- }
377
- if (tagAuthority.tag !== postPublish.tag || tagAuthority.commit !== postPublish.tagCommit) {
378
- throw new ReleaseError(
379
- GATE_FAILED,
380
- 'frozen postPublish tag identity conflicts with the frozen create-tag binding',
381
- {
382
- postPublishTag: postPublish.tag,
383
- postPublishTagCommit: postPublish.tagCommit,
384
- bindingTag: tagAuthority.tag,
385
- bindingCommit: tagAuthority.commit,
386
- },
387
- );
379
+ for (const prep of prepared) {
380
+ const { declaration } = prep;
381
+ if (declaration.payloadSource !== PAYLOAD_SOURCE_TAG_WORKTREE) {
382
+ throw new ReleaseError(
383
+ GATE_FAILED,
384
+ `postPublish.payloadSource must be "${PAYLOAD_SOURCE_TAG_WORKTREE}"`,
385
+ { payloadSource: declaration.payloadSource, unitId: declaration.unitId },
386
+ );
387
+ }
388
+ if (typeof declaration.tagCommit !== 'string' || !SHA_RE.test(declaration.tagCommit)) {
389
+ throw new ReleaseError(
390
+ GATE_FAILED,
391
+ 'postPublish is missing a frozen tagCommit binding; postVerify fails closed',
392
+ { tagCommit: declaration.tagCommit ?? null, unitId: declaration.unitId },
393
+ );
394
+ }
395
+ let tagAuthority;
396
+ try {
397
+ tagAuthority = await resolveFrozenTagAuthority({
398
+ plan,
399
+ unitId: declaration.unitId,
400
+ // T04 (裁决 14): the tag proof is taken from the legitimate PUBLISHED
401
+ // parent run — never from the verify receipt.
402
+ sourceRun: publishRun,
403
+ root,
404
+ exec,
405
+ observeTagFn,
406
+ observeBranchFn,
407
+ });
408
+ } catch (err) {
409
+ throw err instanceof ReleaseError ? err : new ReleaseError(
410
+ GATE_FAILED,
411
+ `tag identity cannot be verified: ${err?.message ?? err}`,
412
+ );
413
+ }
414
+ if (tagAuthority.tag !== declaration.tag || tagAuthority.commit !== declaration.tagCommit) {
415
+ throw new ReleaseError(
416
+ GATE_FAILED,
417
+ 'frozen postPublish tag identity conflicts with the frozen create-tag binding',
418
+ {
419
+ postPublishTag: declaration.tag,
420
+ postPublishTagCommit: declaration.tagCommit,
421
+ bindingTag: tagAuthority.tag,
422
+ bindingCommit: tagAuthority.commit,
423
+ unitId: declaration.unitId,
424
+ },
425
+ );
426
+ }
427
+ prep.tagAuthority = tagAuthority;
388
428
  }
389
429
 
390
430
  // =========================================================================
@@ -405,12 +445,12 @@ export async function postVerifyRelease(options) {
405
445
  const startedAt = clockFn();
406
446
  let stateSequence = -1;
407
447
 
408
- const checkpoints = postVerifyHooks.map((hook) => ({
409
- actionId: hook.id,
448
+ const checkpoints = prepared.flatMap((prep) => prep.postVerifyHooks.map((hook) => ({
449
+ actionId: unitActionId(prep.declaration.unitId, hook.id),
410
450
  actionType: 'postpublish-hook',
411
451
  status: 'PENDING',
412
452
  executor: EXECUTOR,
413
- }));
453
+ })));
414
454
  const checkpointById = new Map(checkpoints.map((cp) => [cp.actionId, cp]));
415
455
 
416
456
  const buildPersistedState = (status, finishedAt) => ({
@@ -450,11 +490,20 @@ export async function postVerifyRelease(options) {
450
490
  return appendRunState(runDir, stateSequence, buildPersistedState(status));
451
491
  };
452
492
 
453
- /** Persist the BLOCKED run record (idempotent) once the run exists. */
454
- const recordBlocked = async () => {
493
+ // External-success facts (declared before recordFailure: an exception after
494
+ // a succeeded hook must persist PARTIAL, never BLOCKED — rework R-01).
495
+ let externalSuccesses = 0;
496
+
497
+ /**
498
+ * Persist the failure run record (idempotent) once the run exists.
499
+ * Rework R-01: PARTIAL when an external checkpoint already succeeded (the
500
+ * success checkpoint is retained), BLOCKED at zero side effects.
501
+ */
502
+ const recordFailure = async () => {
455
503
  if (finalRecordWritten) return;
504
+ const status = externalSuccesses > 0 ? PARTIAL : BLOCKED;
456
505
  try {
457
- await writeRunAtomic(runPath, buildPersistedState(BLOCKED, clockFn()));
506
+ await writeRunAtomic(runPath, buildPersistedState(status, clockFn()));
458
507
  finalRecordWritten = true;
459
508
  } catch {
460
509
  // Persistence must never mask the primary failure.
@@ -488,6 +537,52 @@ export async function postVerifyRelease(options) {
488
537
  // Durable pre-execute authority (seq 0).
489
538
  await snapshot(DISTRIBUTING);
490
539
 
540
+ // =======================================================================
541
+ // R-01 (rework): preset deterministic-conflict preflight — the narrow
542
+ // READ-ONLY entry (core/preset-executor.mjs preflightPresetHook) calls
543
+ // the SAME observation implementation the execution-phase delivery uses,
544
+ // for every postVerify hook that will actually execute this run. A
545
+ // deterministic conflict (e.g. the proposal path already exists with
546
+ // different bytes) fails the whole run here, BEFORE the first hook. This
547
+ // is not a lock: the execution phase re-observes every remote regardless.
548
+ // Hooks parked at AWAITING_APPROVAL are not observed here — the approved
549
+ // rerun observes them at execution time.
550
+ // =======================================================================
551
+ for (const prep of prepared) {
552
+ const { declaration, postVerifyHooks: preflightHooks } = prep;
553
+ for (const hook of preflightHooks) {
554
+ if (effectiveHookRequiresApproval(hook) && !approvedHookIds.has(hook.id)) continue;
555
+ const proposalProjection = buildPostPublishContext({
556
+ plan,
557
+ postPublish: declaration,
558
+ runId: `postverify-${verifyRun.runId}`,
559
+ sourceRun: publishRun,
560
+ payloadDir: undefined,
561
+ phase: 'postVerify',
562
+ verifyEvidence: {
563
+ runId: verifyRun.runId,
564
+ status: verifyRun.status,
565
+ finishedAt: verifyRun.finishedAt,
566
+ },
567
+ });
568
+ const observation = await preflightPresetHook({
569
+ hook,
570
+ contextProjection: proposalProjection,
571
+ exec,
572
+ });
573
+ if (observation !== null) {
574
+ await evidence.append({
575
+ phase: 'safety-gate',
576
+ gate: 'preset-preflight',
577
+ status: 'passed',
578
+ hookId: hook.id,
579
+ unitId: declaration.unitId,
580
+ verdict: observation.verdict,
581
+ });
582
+ }
583
+ }
584
+ }
585
+
491
586
  // =======================================================================
492
587
  // F-04 root split: postVerify holds TWO distinct roots —
493
588
  // - releaseWorkspaceRoot: the real project root the user releases from;
@@ -502,7 +597,7 @@ export async function postVerifyRelease(options) {
502
597
  try {
503
598
  releaseWorkspaceRoot = await realpath(root);
504
599
  } catch (err) {
505
- await recordBlocked();
600
+ await recordFailure();
506
601
  throw new ReleaseError(
507
602
  GATE_FAILED,
508
603
  `release workspace root does not resolve to an existing directory: ${err.message}`,
@@ -510,107 +605,113 @@ export async function postVerifyRelease(options) {
510
605
  );
511
606
  }
512
607
 
513
- // R1 timing contract carries over: hooks run inside a detached worktree
514
- // at the frozen tagCommit, never in the live workspace. R-01: the
515
- // worktree is materialized from a COPY of the frozen bare repo (never
516
- // registered in the source repo's worktree metadata). Dry-run executes
517
- // nothing, so no worktree is allocated for a rehearsal.
518
- if (dryRun !== true) {
519
- try {
520
- tmpBase = await mkdtemp(join(tmpdir(), 'release-skill-postverify-'));
521
- ({ worktreePath } = await createFrozenTagWorktree({
522
- gitDir: tagAuthority.gitDir,
523
- commit: tagAuthority.commit,
524
- tmpBase,
525
- exec,
526
- }));
527
- } catch (err) {
528
- await evidence.append({
529
- phase: 'worktree',
530
- status: 'failed',
531
- error: asError('WORKTREE_FAILED', boundedOutputTail(err?.stderr ?? err?.message)),
532
- });
533
- await recordBlocked();
534
- throw new ReleaseError(
535
- GATE_FAILED,
536
- `cannot create the detached tag worktree at ${postPublish.tagCommit}: ${err?.message ?? err}`,
537
- { tagCommit: postPublish.tagCommit },
538
- );
539
- }
540
- await evidence.append({ phase: 'worktree', status: 'passed' });
541
-
542
- // =====================================================================
543
- // Private execution bundle (F-01 / T1): postVerify re-entry consumes
544
- // the SAME frozen bundle bytes — never the live workspace copies.
545
- // Strictly re-read the digest-addressed bytes, recompute the closure
546
- // through Foundation, and install ONLY the verified bytes into the
547
- // fresh tag worktree before any hook runs; any mismatch fails closed
548
- // before a hook or an external write.
549
- // =====================================================================
550
- let installedBundlePaths = [];
551
- try {
552
- ({ installed: installedBundlePaths } = await verifyAndInstallExecutionBundle({
553
- plan,
554
- planPath,
555
- worktreePath,
556
- }));
557
- } catch (err) {
608
+ // Saga-level chain state (single run authority; the source VERIFIED
609
+ // verify run is never touched a partial postVerify never downgrades it).
610
+ let hooksStopped = false;
611
+ let failures = 0;
612
+ let awaitingApproval = 0;
613
+
614
+ // =========================================================================
615
+ // postVerify hooks, per declaration in frozen order. A failure stops the
616
+ // chain; a requiresApproval hook without a checkpoint approval parks at
617
+ // AWAITING_APPROVAL and never executes; dry-run executes nothing.
618
+ // =========================================================================
619
+ for (const prep of prepared) {
620
+ const { declaration, postVerifyHooks: declarationHooks } = prep;
621
+ const tagAuthority = prep.tagAuthority;
622
+
623
+ // R1 timing contract carries over: hooks run inside a detached worktree
624
+ // at the frozen tagCommit, never in the live workspace. R-01: the
625
+ // worktree is materialized from a COPY of the frozen bare repo (never
626
+ // registered in the source repo's worktree metadata). Dry-run executes
627
+ // nothing, so no worktree is allocated for a rehearsal.
628
+ if (dryRun !== true) {
629
+ try {
630
+ tmpBase = await mkdtemp(join(tmpdir(), 'release-skill-postverify-'));
631
+ ({ worktreePath } = await createFrozenTagWorktree({
632
+ gitDir: tagAuthority.gitDir,
633
+ commit: tagAuthority.commit,
634
+ tmpBase,
635
+ exec,
636
+ }));
637
+ } catch (err) {
638
+ await evidence.append({
639
+ phase: 'worktree',
640
+ status: 'failed',
641
+ error: asError('WORKTREE_FAILED', boundedOutputTail(err?.stderr ?? err?.message)),
642
+ });
643
+ await recordFailure();
644
+ throw new ReleaseError(
645
+ GATE_FAILED,
646
+ `cannot create the detached tag worktree at ${declaration.tagCommit}: ${err?.message ?? err}`,
647
+ { tagCommit: declaration.tagCommit },
648
+ );
649
+ }
650
+ await evidence.append({ phase: 'worktree', status: 'passed' });
651
+
652
+ // ===================================================================
653
+ // Private execution bundle (F-01 / T1): postVerify re-entry consumes
654
+ // the SAME frozen bundle bytes — never the live workspace copies.
655
+ // Strictly re-read the digest-addressed bytes, recompute the closure
656
+ // through Foundation, and install ONLY the verified bytes into the
657
+ // fresh tag worktree before any hook runs; any mismatch fails closed
658
+ // before a hook or an external write.
659
+ // ===================================================================
660
+ let installedBundlePaths = [];
661
+ try {
662
+ ({ installed: installedBundlePaths } = await verifyAndInstallExecutionBundle({
663
+ planPath,
664
+ worktreePath,
665
+ postPublish: declaration,
666
+ }));
667
+ } catch (err) {
668
+ await evidence.append({
669
+ phase: 'worktree',
670
+ gate: 'execution-bundle',
671
+ status: 'failed',
672
+ error: asError('EXECUTION_BUNDLE_FAILED', boundedOutputTail(err?.message ?? String(err))),
673
+ });
674
+ await recordFailure();
675
+ throw err instanceof ReleaseError ? err : new ReleaseError(
676
+ GATE_FAILED,
677
+ `cannot verify the frozen execution bundle: ${err?.message ?? err}`,
678
+ );
679
+ }
558
680
  await evidence.append({
559
681
  phase: 'worktree',
560
682
  gate: 'execution-bundle',
561
- status: 'failed',
562
- error: asError('EXECUTION_BUNDLE_FAILED', boundedOutputTail(err?.message ?? String(err))),
683
+ status: 'passed',
684
+ installed: installedBundlePaths,
563
685
  });
564
- await recordBlocked();
565
- throw err instanceof ReleaseError ? err : new ReleaseError(
566
- GATE_FAILED,
567
- `cannot verify the frozen execution bundle: ${err?.message ?? err}`,
568
- );
569
686
  }
570
- await evidence.append({
571
- phase: 'worktree',
572
- gate: 'execution-bundle',
573
- status: 'passed',
574
- installed: installedBundlePaths,
575
- });
576
- }
577
-
578
- // §2.3 context projection: verifyEvidence PRESENT (postVerify phase);
579
- // publishedAt from the sealed publish run; payloadDir never travels.
580
- const verifyEvidence = {
581
- runId: verifyRun.runId,
582
- status: verifyRun.status,
583
- finishedAt: verifyRun.finishedAt,
584
- };
585
- const hookContextProjection = buildPostPublishContext({
586
- plan,
587
- runId,
588
- sourceRun: publishRun,
589
- payloadDir: undefined,
590
- phase: 'postVerify',
591
- verifyEvidence,
592
- });
593
687
 
594
- // Proposal documents must stay byte-deterministic across redeliveries of
595
- // the SAME release event (NO_CHANGE idempotence): they travel with the
596
- // stable lineage-derived event identity, not the per-attempt runId.
597
- const proposalContextProjection = {
598
- ...hookContextProjection,
599
- runId: `postverify-${verifyRun.runId}`,
600
- };
688
+ // §2.3 context projection: verifyEvidence PRESENT (postVerify phase);
689
+ // publishedAt from the sealed publish run; payloadDir never travels.
690
+ const verifyEvidence = {
691
+ runId: verifyRun.runId,
692
+ status: verifyRun.status,
693
+ finishedAt: verifyRun.finishedAt,
694
+ };
695
+ const hookContextProjection = buildPostPublishContext({
696
+ plan,
697
+ postPublish: declaration,
698
+ runId,
699
+ sourceRun: publishRun,
700
+ payloadDir: undefined,
701
+ phase: 'postVerify',
702
+ verifyEvidence,
703
+ });
601
704
 
602
- // =========================================================================
603
- // postVerify hooks, declared order. A failure stops the chain; a
604
- // requiresApproval hook without a checkpoint approval parks at
605
- // AWAITING_APPROVAL and never executes; dry-run executes nothing.
606
- // =========================================================================
607
- let hooksStopped = false;
608
- let failures = 0;
609
- let awaitingApproval = 0;
610
- let externalSuccesses = 0;
705
+ // Proposal documents must stay byte-deterministic across redeliveries of
706
+ // the SAME release event (NO_CHANGE idempotence): they travel with the
707
+ // stable lineage-derived event identity, not the per-attempt runId.
708
+ const proposalContextProjection = {
709
+ ...hookContextProjection,
710
+ runId: `postverify-${verifyRun.runId}`,
711
+ };
611
712
 
612
- for (const hook of postVerifyHooks) {
613
- const cp = checkpointById.get(hook.id);
713
+ for (const hook of declarationHooks) {
714
+ const cp = checkpointById.get(unitActionId(declaration.unitId, hook.id));
614
715
  cp.startedAt = clockFn();
615
716
 
616
717
  if (hooksStopped) {
@@ -657,7 +758,7 @@ export async function postVerifyRelease(options) {
657
758
  hook,
658
759
  contextProjection: hookContextProjection,
659
760
  proposalContextProjection,
660
- commitIdentity: postPublish.commitIdentity,
761
+ commitIdentity: declaration.commitIdentity,
661
762
  // F-04: presets receive the RELEASE workspace root (target.workspace
662
763
  // resolution + release-workspace write exclusion). The detached
663
764
  // worktree is the execution worktree and never impersonates it.
@@ -743,6 +844,7 @@ export async function postVerifyRelease(options) {
743
844
  try {
744
845
  hookExecution = await hookRunner(
745
846
  {
847
+ id: hook.id,
746
848
  command: hook.command,
747
849
  ...(hook.cwd ? { cwd: hook.cwd } : {}),
748
850
  ...(hook.timeoutMs !== undefined ? { timeoutMs: hook.timeoutMs } : {}),
@@ -802,6 +904,11 @@ export async function postVerifyRelease(options) {
802
904
  await snapshot(PARTIAL);
803
905
  }
804
906
 
907
+ // This declaration's scratch worktree is consumed; release it. The
908
+ // outer finally keeps the safety net for a mid-declaration throw.
909
+ await cleanupWorktree();
910
+ }
911
+
805
912
  // =========================================================================
806
913
  // Classification (returned, not thrown) — distribute saga family:
807
914
  // - DISTRIBUTED: no failures and no awaiting-approval hooks;
@@ -846,12 +953,12 @@ export async function postVerifyRelease(options) {
846
953
  status: 'failed',
847
954
  error: { code: err.code, message: err.message },
848
955
  });
849
- if (!finalRecordWritten) await recordBlocked();
956
+ if (!finalRecordWritten) await recordFailure();
850
957
  } catch {
851
958
  // Persistence must never mask the primary failure.
852
959
  }
853
960
  await evidence.finish({
854
- status: BLOCKED,
961
+ status: externalSuccesses > 0 ? PARTIAL : BLOCKED,
855
962
  error: { code: err.code, message: err.message },
856
963
  failedAt: clockFn(),
857
964
  }).catch(() => {});