release-skill 0.1.10 → 0.2.1

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 (82) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +33 -0
  7. package/INSTALL.md +24 -4
  8. package/INSTALL.zh-CN.md +22 -4
  9. package/README.md +19 -32
  10. package/README.zh-CN.md +14 -25
  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/release-skill.bundle.mjs +2721 -1843
  14. package/adapters/claude/schemas/.render-manifest.json +8 -8
  15. package/adapters/claude/schemas/approval-record.schema.json +1 -1
  16. package/adapters/claude/schemas/release-plan.schema.json +6 -2
  17. package/adapters/claude/schemas/release-project.schema.json +14 -0
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +2721 -1843
  20. package/adapters/codex/schemas/.render-manifest.json +8 -8
  21. package/adapters/codex/schemas/approval-record.schema.json +1 -1
  22. package/adapters/codex/schemas/release-plan.schema.json +6 -2
  23. package/adapters/codex/schemas/release-project.schema.json +14 -0
  24. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  25. package/adapters/kimi/bin/release-skill.bundle.mjs +2721 -1843
  26. package/adapters/kimi/schemas/.render-manifest.json +8 -8
  27. package/adapters/kimi/schemas/approval-record.schema.json +1 -1
  28. package/adapters/kimi/schemas/release-plan.schema.json +6 -2
  29. package/adapters/kimi/schemas/release-project.schema.json +14 -0
  30. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +10 -0
  31. package/adapters/workbuddy/bin/release-skill.bundle.mjs +85363 -0
  32. package/adapters/workbuddy/bin/release-skill.mjs +54 -0
  33. package/adapters/workbuddy/native/safe-write/binding.gyp +41 -0
  34. package/adapters/workbuddy/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  35. package/adapters/workbuddy/native/safe-write/prebuilds.json +24 -0
  36. package/adapters/workbuddy/native/safe-write/src/safe_write.cc +2032 -0
  37. package/adapters/workbuddy/schemas/.render-manifest.json +37 -0
  38. package/adapters/workbuddy/schemas/approval-record.schema.json +115 -0
  39. package/adapters/workbuddy/schemas/artifact-lock.schema.json +111 -0
  40. package/adapters/workbuddy/schemas/artifact-plan.schema.json +52 -0
  41. package/adapters/workbuddy/schemas/artifact-policy.schema.json +76 -0
  42. package/adapters/workbuddy/schemas/evidence-event.schema.json +89 -0
  43. package/adapters/workbuddy/schemas/release-plan.schema.json +882 -0
  44. package/adapters/workbuddy/schemas/release-project.schema.json +909 -0
  45. package/adapters/workbuddy/schemas/release-run.schema.json +343 -0
  46. package/adapters/workbuddy/skills/release-assess/SKILL.md +51 -0
  47. package/adapters/workbuddy/skills/release-help/SKILL.md +77 -0
  48. package/adapters/workbuddy/skills/release-prepare/SKILL.md +92 -0
  49. package/adapters/workbuddy/skills/release-publish/SKILL.md +57 -0
  50. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +73 -0
  51. package/adapters/workbuddy/skills/release-setup/SKILL.md +95 -0
  52. package/adapters/workbuddy/skills/release-verify/SKILL.md +70 -0
  53. package/bin/release-skill-cli.mjs +3 -0
  54. package/bin/release-skill.bundle.mjs +2721 -1843
  55. package/package.json +9 -2
  56. package/references/.render-manifest.json +8 -8
  57. package/references/01-state-machine.md +5 -5
  58. package/references/02-project-config.md +1 -1
  59. package/references/05-evidence-and-errors.md +1 -1
  60. package/references/06-adapter-contract.md +41 -1
  61. package/schemas/.render-manifest.json +8 -8
  62. package/schemas/approval-record.schema.json +1 -1
  63. package/schemas/release-plan.schema.json +6 -2
  64. package/schemas/release-project.schema.json +14 -0
  65. package/scripts/sync-public-files.mjs +481 -0
  66. package/src/adapters/contract.mjs +60 -0
  67. package/src/adapters/plugin-marketplace.mjs +289 -736
  68. package/src/commands/prepare.mjs +195 -182
  69. package/src/commands/publish.mjs +438 -122
  70. package/src/commands/reconcile.mjs +369 -191
  71. package/src/commands/verify.mjs +13 -2
  72. package/src/core/approval.mjs +72 -45
  73. package/src/core/baseline.mjs +5 -0
  74. package/src/core/checkpoints.mjs +143 -0
  75. package/src/core/evidence.mjs +30 -3
  76. package/src/core/hook-cache.mjs +254 -0
  77. package/src/core/hooks.mjs +37 -1
  78. package/src/core/observe-retry.mjs +223 -0
  79. package/src/core/plan.mjs +162 -253
  80. package/src/platforms/kimi.mjs +514 -0
  81. package/src/platforms/registry.mjs +393 -0
  82. package/src/producers/build-adapters.mjs +49 -23
@@ -39,6 +39,7 @@ import {
39
39
  reObservePreviousPublicBaseline,
40
40
  } from '../core/previous-public-baseline.mjs';
41
41
  import { createEvidenceWriter } from '../core/evidence.mjs';
42
+ import { CHECKPOINT_ORDER, ADAPTER_ACTION_TYPE_MAP } from '../core/checkpoints.mjs';
42
43
  import {
43
44
  loadRun,
44
45
  validateRunPlanDigest,
@@ -59,42 +60,15 @@ import {
59
60
  } from '../core/errors.mjs';
60
61
  import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-machine.mjs';
61
62
  import { matchObservation } from '../adapters/contract.mjs';
63
+ import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY } from '../core/observe-retry.mjs';
62
64
 
63
65
  // ---------------------------------------------------------------------------
64
66
  // Constants
65
67
  // ---------------------------------------------------------------------------
66
68
 
67
- /**
68
- * Checkpoint order for the reconcile saga.
69
- * Must match publish.mjs.
70
- */
71
- const CHECKPOINT_ORDER = [
72
- 'push-commit',
73
- 'push-snapshot',
74
- 'set-default-branch',
75
- 'create-tag',
76
- 'npm-publish',
77
- 'github-release',
78
- 'claude-marketplace-install',
79
- 'codex-marketplace-install',
80
- 'kimi-marketplace-install',
81
- ];
82
-
83
- /**
84
- * Map plan action type to adapter ActionType.
85
- * Must match publish.mjs.
86
- */
87
- const ADAPTER_ACTION_TYPE_MAP = {
88
- 'push-commit': 'git-push',
89
- 'push-snapshot': 'push-snapshot',
90
- 'set-default-branch': 'set-default-branch',
91
- 'create-tag': 'git-tag',
92
- 'npm-publish': 'npm-publish',
93
- 'github-release': 'github-release',
94
- 'claude-marketplace-install': 'claude-marketplace-install',
95
- 'codex-marketplace-install': 'codex-marketplace-install',
96
- 'kimi-marketplace-install': 'kimi-marketplace-install',
97
- };
69
+ // CHECKPOINT_ORDER and ADAPTER_ACTION_TYPE_MAP live in ../core/checkpoints.mjs
70
+ // (single source shared with publish.mjs; T3.1 §4.7). The historical
71
+ // `Must match publish.mjs` double-write is removed.
98
72
 
99
73
  /**
100
74
  * Marketplace action types that are reconstructable isolated consumer checks,
@@ -151,6 +125,7 @@ export async function reconcileRelease(options) {
151
125
  captureBaselineFn,
152
126
  productionConfirmation,
153
127
  observePreviousPublicBaselineFn,
128
+ observeRetrySleep,
154
129
  } = options ?? {};
155
130
 
156
131
  const clockFn = typeof clockOpt === 'function' ? clockOpt : defaultClock;
@@ -364,31 +339,62 @@ export async function reconcileRelease(options) {
364
339
 
365
340
  const currentBaseline = await captureBaselineActual(root);
366
341
 
342
+ // planVersion fork (design: t1-2-digest-decoupling.md §4.3): for v2
343
+ // plans the baseline is record-layer audit data. Drift is recorded as a
344
+ // warning and execution continues -- artifact integrity is sealed by the
345
+ // frozen-artifact re-verification, never by workspace equality. v1 plans
346
+ // keep the BASELINE_CHANGED hard failure, byte for byte.
347
+ const planV2 = plan.planVersion === 2;
348
+
367
349
  if (currentBaseline.gitTreeHash !== plan.baseline.gitTreeHash) {
368
- await evidence.append({
369
- phase: 'safety-gate',
370
- gate: 'baseline-check',
371
- status: 'failed',
372
- planTreeHash: plan.baseline.gitTreeHash,
373
- currentTreeHash: currentBaseline.gitTreeHash,
374
- });
350
+ if (planV2) {
351
+ await evidence.append({
352
+ phase: 'safety-gate',
353
+ gate: 'baseline-check',
354
+ status: 'warning',
355
+ severity: 'warning',
356
+ reason: 'planVersion 2: baseline drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
357
+ planTreeHash: plan.baseline.gitTreeHash,
358
+ currentTreeHash: currentBaseline.gitTreeHash,
359
+ });
360
+ } else {
361
+ await evidence.append({
362
+ phase: 'safety-gate',
363
+ gate: 'baseline-check',
364
+ status: 'failed',
365
+ planTreeHash: plan.baseline.gitTreeHash,
366
+ currentTreeHash: currentBaseline.gitTreeHash,
367
+ });
375
368
 
376
- throw new ReleaseError(
377
- BASELINE_CHANGED,
378
- `baseline has changed since plan freeze: plan=${plan.baseline.gitTreeHash}, current=${currentBaseline.gitTreeHash}`,
379
- { planTreeHash: plan.baseline.gitTreeHash, currentTreeHash: currentBaseline.gitTreeHash },
380
- );
369
+ throw new ReleaseError(
370
+ BASELINE_CHANGED,
371
+ `baseline has changed since plan freeze: plan=${plan.baseline.gitTreeHash}, current=${currentBaseline.gitTreeHash}`,
372
+ { planTreeHash: plan.baseline.gitTreeHash, currentTreeHash: currentBaseline.gitTreeHash },
373
+ );
374
+ }
381
375
  }
382
376
 
383
377
  if (
384
378
  plan.baseline.workspaceDigest &&
385
379
  currentBaseline.workspaceDigest !== plan.baseline.workspaceDigest
386
380
  ) {
387
- throw new ReleaseError(
388
- BASELINE_CHANGED,
389
- `workspace digest has changed since plan freeze: plan=${plan.baseline.workspaceDigest}, current=${currentBaseline.workspaceDigest}`,
390
- { planWorkspaceDigest: plan.baseline.workspaceDigest, currentWorkspaceDigest: currentBaseline.workspaceDigest },
391
- );
381
+ if (planV2) {
382
+ await evidence.append({
383
+ phase: 'safety-gate',
384
+ gate: 'baseline-check',
385
+ status: 'warning',
386
+ severity: 'warning',
387
+ reason: 'planVersion 2: workspace digest drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
388
+ planWorkspaceDigest: plan.baseline.workspaceDigest,
389
+ currentWorkspaceDigest: currentBaseline.workspaceDigest,
390
+ });
391
+ } else {
392
+ throw new ReleaseError(
393
+ BASELINE_CHANGED,
394
+ `workspace digest has changed since plan freeze: plan=${plan.baseline.workspaceDigest}, current=${currentBaseline.workspaceDigest}`,
395
+ { planWorkspaceDigest: plan.baseline.workspaceDigest, currentWorkspaceDigest: currentBaseline.workspaceDigest },
396
+ );
397
+ }
392
398
  }
393
399
 
394
400
  await evidence.append({
@@ -559,15 +565,21 @@ export async function reconcileRelease(options) {
559
565
  // --- Phase 1: Process each plan action using source run checkpoint ---
560
566
  //
561
567
  // Marketplace actions are reconstructable isolated consumer checks, not
562
- // permanent remote writes. They are handled with a fresh
563
- // preflight -> execute -> verify cycle in an isolated directory, using
564
- // isolatedConsumerWritesAuthorized instead of externalWritesAuthorized.
568
+ // permanent remote writes. Their recovery is DEFERRED to phase 4 (after
569
+ // the non-marketplace remote-write retry loop): github-release is the
570
+ // prerequisite for kimi's install URL, and a marketplace failure must
571
+ // never block pending remote-write retries.
565
572
  //
566
573
  // Non-marketplace actions use observe-based consistency checks.
567
574
  // =======================================================================
568
575
  const actionsToRetry = [];
576
+ const marketplaceRetriesToRetry = [];
569
577
  const actionResults = new Map(); // actionId -> final status
570
- let retryFailed = false;
578
+ // Remote-write retries and marketplace recovery are independent failure
579
+ // groups: neither group's failure blocks the other group's retries, and
580
+ // either failure keeps the run PARTIAL.
581
+ let remoteWriteRetryFailed = false;
582
+ let marketplaceRetryFailed = false;
571
583
 
572
584
  for (const action of planActions) {
573
585
  const sourceCp = sourceCpMap.get(action.id);
@@ -582,103 +594,11 @@ export async function reconcileRelease(options) {
582
594
  const adapter = adapterRegistry.getAdapter(adapterActionType);
583
595
 
584
596
  // -------------------------------------------------------------------
585
- // Marketplace actions: isolated consumer verification
597
+ // Marketplace actions: deferred to the dedicated recovery phase that
598
+ // runs AFTER the non-marketplace remote-write retry loop (phase 4).
599
+ // A marketplace failure must never block pending remote-write retries.
586
600
  // -------------------------------------------------------------------
587
601
  if (MARKETPLACE_TYPES.has(action.type)) {
588
- const marketplaceContext = {
589
- externalWritesAuthorized: false,
590
- isolatedConsumerWritesAuthorized: true,
591
- plan,
592
- baseline: plan.baseline,
593
- root,
594
- runDir,
595
- };
596
-
597
- const actionInput = {
598
- actionType: adapterActionType,
599
- ...action.parameters,
600
- };
601
-
602
- // Preflight (always runs — validates frozen snapshot)
603
- const preflightResult = await adapter.preflight(actionInput, marketplaceContext);
604
- if (preflightResult.status === 'PREFLIGHT_FAILED') {
605
- actionResults.set(action.id, 'failed');
606
- await evidence.append({
607
- phase: 'reconcile-marketplace',
608
- actionId: action.id,
609
- actionType: action.type,
610
- decision: 'preflight-failed',
611
- error: preflightResult.error,
612
- });
613
- throw new ReleaseError(
614
- POST_PUBLISH_VERIFY_FAILED,
615
- `marketplace preflight failed for action "${action.id}": ${preflightResult.error}`,
616
- { actionId: action.id },
617
- );
618
- }
619
-
620
- // Execute (install to isolated consumer directory)
621
- const executeResult = await adapter.execute(actionInput, marketplaceContext);
622
- if (executeResult.status !== 'EXECUTED') {
623
- actionResults.set(action.id, 'failed');
624
- await evidence.append({
625
- phase: 'reconcile-marketplace',
626
- actionId: action.id,
627
- actionType: action.type,
628
- decision: 'execute-failed',
629
- error: executeResult.error,
630
- });
631
- // Execute failed — if source was succeeded this is a conflict;
632
- // if source was failed/pending, add to retry list
633
- if (sourceCp.status === 'succeeded') {
634
- throw new ReleaseError(
635
- REMOTE_CONFLICT,
636
- `marketplace execute failed for SUCCEEDED action "${action.id}": ${executeResult.error}`,
637
- { actionId: action.id },
638
- );
639
- }
640
- actionsToRetry.push(action);
641
- continue;
642
- }
643
-
644
- // Verify (observe + match against plan expected state)
645
- const verifyResult = await adapter.verify(
646
- { ...actionInput, expected: action.expected },
647
- marketplaceContext,
648
- );
649
-
650
- if (verifyResult.status === 'VERIFIED') {
651
- // Consistent: source succeeded => skipped, source failed => succeeded (recovered)
652
- const resultStatus = sourceCp.status === 'succeeded' ? 'skipped' : 'succeeded';
653
- actionResults.set(action.id, resultStatus);
654
- await evidence.append({
655
- phase: 'reconcile-marketplace',
656
- actionId: action.id,
657
- actionType: action.type,
658
- decision: sourceCp.status === 'succeeded' ? 'skip-source-succeeded-consistent' : 'recovered',
659
- sourceStatus: sourceCp.status,
660
- });
661
- } else {
662
- // Verify mismatch
663
- await evidence.append({
664
- phase: 'reconcile-marketplace',
665
- actionId: action.id,
666
- actionType: action.type,
667
- decision: 'verify-mismatch',
668
- error: verifyResult.error,
669
- });
670
- if (sourceCp.status === 'succeeded') {
671
- throw new ReleaseError(
672
- REMOTE_CONFLICT,
673
- `marketplace verify mismatch for SUCCEEDED action "${action.id}": ${verifyResult.error}`,
674
- { actionId: action.id },
675
- );
676
- }
677
- // Source was failed/pending and verify mismatched after retry => BLOCKED
678
- actionResults.set(action.id, 'failed');
679
- retryFailed = true;
680
- // Don't add to retry — this is a verify mismatch, not a missing state
681
- }
682
602
  continue;
683
603
  }
684
604
 
@@ -805,11 +725,30 @@ export async function reconcileRelease(options) {
805
725
  continue;
806
726
  }
807
727
 
808
- // FAILED or PENDING: observe remote state
809
- const observeResult = await adapter.observe(
810
- { actionType: adapterActionType, ...action.parameters },
728
+ // FAILED or PENDING: observe remote state (PROPAGATING retry, T1.1)
729
+ const observeInput = { actionType: adapterActionType, ...action.parameters };
730
+ const observePolicy = MARKETPLACE_TYPES.has(action.type)
731
+ ? clampPolicyToTimeout(DEFAULT_OBSERVE_RETRY_POLICY, action.parameters?.timeoutMs)
732
+ : DEFAULT_OBSERVE_RETRY_POLICY;
733
+ const observeRetry = await observeWithRetry({
734
+ observe: (act, ctx) => adapter.observe(act, ctx),
735
+ action: observeInput,
811
736
  context,
812
- );
737
+ policy: observePolicy,
738
+ sleep: observeRetrySleep,
739
+ onAttempt: (info) => evidence.append({
740
+ phase: 'reconcile-observe-retry',
741
+ actionId: action.id,
742
+ actionType: action.type,
743
+ attempt: info.attempt,
744
+ maxAttempts: info.maxAttempts,
745
+ missing: info.missing,
746
+ delayMs: info.delayMs,
747
+ status: info.missing ? 'propagating' : 'resolved',
748
+ error: info.error ?? null,
749
+ }),
750
+ });
751
+ const observeResult = observeRetry.result;
813
752
 
814
753
  if (
815
754
  !observeResult.observation ||
@@ -941,12 +880,12 @@ export async function reconcileRelease(options) {
941
880
 
942
881
  // --- Phase 2: Validate approval and global preflight before retrying ---
943
882
  //
944
- // Marketplace actions in the retry list do NOT require approval or
945
- // productionConfirmation; they are isolated consumer checks, not
946
- // permanent remote writes. Only non-marketplace retries require approval.
883
+ // Marketplace recovery is deferred to phase 4 and does NOT require
884
+ // approval or productionConfirmation; marketplace actions are isolated
885
+ // consumer checks, not permanent remote writes. Only non-marketplace
886
+ // retries require approval.
947
887
  // =======================================================================
948
- // Split retry actions into marketplace and non-marketplace
949
- const marketplaceRetries = actionsToRetry.filter((a) => MARKETPLACE_TYPES.has(a.type));
888
+ // Phase 1 only collects non-marketplace retry candidates.
950
889
  const nonMarketplaceRetries = actionsToRetry.filter((a) => !MARKETPLACE_TYPES.has(a.type));
951
890
 
952
891
  if (nonMarketplaceRetries.length > 0) {
@@ -997,11 +936,12 @@ export async function reconcileRelease(options) {
997
936
  );
998
937
 
999
938
  if (preflightResult.status === 'PREFLIGHT_FAILED') {
1000
- // Mark all retry actions as failed, stop
939
+ // Mark all retry actions as failed, stop. This only poisons the
940
+ // remote-write group; marketplace recovery (phase 4) still runs.
1001
941
  for (const retryAction of actionsToRetry) {
1002
942
  actionResults.set(retryAction.id, 'failed');
1003
943
  }
1004
- retryFailed = true;
944
+ remoteWriteRetryFailed = true;
1005
945
 
1006
946
  await evidence.append({
1007
947
  phase: 'reconcile-preflight',
@@ -1013,7 +953,7 @@ export async function reconcileRelease(options) {
1013
953
  }
1014
954
  }
1015
955
 
1016
- if (!retryFailed) {
956
+ if (!remoteWriteRetryFailed) {
1017
957
  await evidence.append({ phase: 'reconcile-preflight', status: 'passed' });
1018
958
  }
1019
959
  }
@@ -1061,13 +1001,16 @@ export async function reconcileRelease(options) {
1061
1001
  );
1062
1002
  return latestRetryState;
1063
1003
  };
1064
- if (!retryFailed && actionsToRetry.length > 0) {
1004
+ if (!remoteWriteRetryFailed && actionsToRetry.length > 0) {
1065
1005
  await persistRetryState('PUBLISHING');
1066
1006
  }
1067
1007
 
1068
- // --- Phase 3: Execute retries ---
1069
- if (!retryFailed && actionsToRetry.length > 0) {
1070
- for (const action of actionsToRetry) {
1008
+ // Execute one homogeneous retry group in plan order. The first failure
1009
+ // stops the group (publish semantics) and marks only that group's own
1010
+ // failure flag; a failure in one group never blocks the other group's
1011
+ // retries.
1012
+ const executeRetryGroup = async (groupActions, markGroupFailed) => {
1013
+ for (const action of groupActions) {
1071
1014
  const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
1072
1015
  const adapter = adapterRegistry.getAdapter(adapterActionType);
1073
1016
 
@@ -1103,15 +1046,42 @@ export async function reconcileRelease(options) {
1103
1046
  if (executeResult.status === 'EXECUTED') {
1104
1047
  // Must verify after execute to check remote/install state
1105
1048
  if (MARKETPLACE_TYPES.has(action.type)) {
1106
- // Marketplace: verify via adapter.verify (observe + matchObservation)
1107
- const verifyResult = await adapter.verify(
1108
- { actionType: adapterActionType, ...action.parameters, expected: action.expected },
1109
- retryCtx,
1049
+ // Marketplace: verify via adapter.verify (observe + matchObservation).
1050
+ // Retry the read-only verify while remote state is information-
1051
+ // insufficient (PROPAGATING, T1.1); a present mismatch is a
1052
+ // real conflict and is never retried.
1053
+ const verifyInput = {
1054
+ actionType: adapterActionType,
1055
+ ...action.parameters,
1056
+ expected: action.expected,
1057
+ };
1058
+ const verifyPolicy = clampPolicyToTimeout(
1059
+ DEFAULT_OBSERVE_RETRY_POLICY,
1060
+ action.parameters?.timeoutMs,
1110
1061
  );
1062
+ const verifyRetry = await observeWithRetry({
1063
+ observe: (act, ctx) => adapter.verify(act, ctx),
1064
+ action: verifyInput,
1065
+ context: retryCtx,
1066
+ policy: verifyPolicy,
1067
+ sleep: observeRetrySleep,
1068
+ onAttempt: (info) => evidence.append({
1069
+ phase: 'reconcile-retry-verify',
1070
+ actionId: action.id,
1071
+ actionType: action.type,
1072
+ attempt: info.attempt,
1073
+ maxAttempts: info.maxAttempts,
1074
+ missing: info.missing,
1075
+ delayMs: info.delayMs,
1076
+ status: info.missing ? 'propagating' : 'resolved',
1077
+ error: info.error ?? null,
1078
+ }),
1079
+ });
1080
+ const verifyResult = verifyRetry.result;
1111
1081
 
1112
1082
  if (verifyResult.status !== 'VERIFIED') {
1113
1083
  actionResults.set(action.id, 'failed');
1114
- retryFailed = true;
1084
+ markGroupFailed();
1115
1085
  await evidence.append({
1116
1086
  phase: 'reconcile-retry',
1117
1087
  actionId: action.id,
@@ -1123,22 +1093,43 @@ export async function reconcileRelease(options) {
1123
1093
  break;
1124
1094
  }
1125
1095
  } else {
1126
- // Non-marketplace: observe after execute to verify remote state
1127
- let observation;
1128
- let observeError;
1129
- try {
1130
- const observeResult = await adapter.observe(
1131
- { actionType: adapterActionType, ...action.parameters, expected: action.expected },
1132
- retryCtx,
1133
- );
1134
- observation = observeResult.observation;
1135
- observeError = observeResult.error;
1136
- } catch (error) {
1137
- observeError = error.message;
1138
- }
1096
+ // Non-marketplace: observe after execute to verify remote state.
1097
+ // execute succeeded, so a missing/uncertain observe may be a
1098
+ // transient propagation delay — retry with bounded backoff
1099
+ // (PROPAGATING, T1.1). A present mismatch is a real
1100
+ // conflict and is never retried.
1101
+ const observeInput = {
1102
+ actionType: adapterActionType,
1103
+ ...action.parameters,
1104
+ expected: action.expected,
1105
+ };
1106
+ const observePolicy = clampPolicyToTimeout(
1107
+ DEFAULT_OBSERVE_RETRY_POLICY,
1108
+ action.parameters?.timeoutMs,
1109
+ );
1110
+ const observeRetry = await observeWithRetry({
1111
+ observe: (act, ctx) => adapter.observe(act, ctx),
1112
+ action: observeInput,
1113
+ context: retryCtx,
1114
+ policy: observePolicy,
1115
+ sleep: observeRetrySleep,
1116
+ onAttempt: (info) => evidence.append({
1117
+ phase: 'reconcile-retry-observe',
1118
+ actionId: action.id,
1119
+ actionType: action.type,
1120
+ attempt: info.attempt,
1121
+ maxAttempts: info.maxAttempts,
1122
+ missing: info.missing,
1123
+ delayMs: info.delayMs,
1124
+ status: info.missing ? 'propagating' : 'resolved',
1125
+ error: info.error ?? null,
1126
+ }),
1127
+ });
1128
+ const observation = observeRetry.result?.observation ?? null;
1129
+ const observeError = observeRetry.result?.error ?? null;
1139
1130
  if (!observation || (observeError && Object.keys(observation).length === 0)) {
1140
1131
  actionResults.set(action.id, 'uncertain');
1141
- retryFailed = true;
1132
+ markGroupFailed();
1142
1133
  await evidence.append({
1143
1134
  phase: 'reconcile-retry',
1144
1135
  actionId: action.id,
@@ -1155,7 +1146,7 @@ export async function reconcileRelease(options) {
1155
1146
  const { matches } = matchObservation(action.expected, observation);
1156
1147
  if (!matches) {
1157
1148
  actionResults.set(action.id, 'failed');
1158
- retryFailed = true;
1149
+ markGroupFailed();
1159
1150
  await evidence.append({
1160
1151
  phase: 'reconcile-retry',
1161
1152
  actionId: action.id,
@@ -1168,7 +1159,7 @@ export async function reconcileRelease(options) {
1168
1159
  }
1169
1160
  } else if (observation && observation.mismatched) {
1170
1161
  actionResults.set(action.id, 'failed');
1171
- retryFailed = true;
1162
+ markGroupFailed();
1172
1163
  await evidence.append({
1173
1164
  phase: 'reconcile-retry',
1174
1165
  actionId: action.id,
@@ -1227,7 +1218,7 @@ export async function reconcileRelease(options) {
1227
1218
  } else {
1228
1219
  actionResults.set(action.id, 'failed');
1229
1220
  }
1230
- retryFailed = true;
1221
+ markGroupFailed();
1231
1222
 
1232
1223
  await evidence.append({
1233
1224
  phase: 'reconcile-retry',
@@ -1240,12 +1231,190 @@ export async function reconcileRelease(options) {
1240
1231
  break;
1241
1232
  }
1242
1233
  }
1234
+ };
1235
+
1236
+ // --- Phase 3: Execute non-marketplace remote-write retries ---
1237
+ //
1238
+ // Runs BEFORE marketplace recovery: github-release is the prerequisite
1239
+ // for kimi's install URL. Gated ONLY on the remote-write group's own
1240
+ // failure flag — a failing marketplace recovery must never block these
1241
+ // retries.
1242
+ if (!remoteWriteRetryFailed && actionsToRetry.length > 0) {
1243
+ await executeRetryGroup(actionsToRetry, () => { remoteWriteRetryFailed = true; });
1244
+ }
1245
+
1246
+ // --- Phase 4: Marketplace recovery (isolated consumer checks) ---
1247
+ //
1248
+ // Deferred until after the remote-write retry loop. Marketplace actions
1249
+ // are reconstructable isolated consumer checks, not permanent remote
1250
+ // writes: each gets a fresh preflight -> execute -> verify cycle in an
1251
+ // isolated directory, using isolatedConsumerWritesAuthorized instead of
1252
+ // externalWritesAuthorized. A failed remote-write retry must not
1253
+ // fabricate or suppress marketplace outcomes, and a marketplace failure
1254
+ // never blocks the remote-write group above.
1255
+ for (const action of planActions) {
1256
+ if (!MARKETPLACE_TYPES.has(action.type)) continue;
1257
+ const sourceCp = sourceCpMap.get(action.id);
1258
+ const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
1259
+ const adapter = adapterRegistry.getAdapter(adapterActionType);
1260
+
1261
+ const marketplaceContext = {
1262
+ externalWritesAuthorized: false,
1263
+ isolatedConsumerWritesAuthorized: true,
1264
+ plan,
1265
+ baseline: plan.baseline,
1266
+ root,
1267
+ runDir,
1268
+ };
1269
+
1270
+ const actionInput = {
1271
+ actionType: adapterActionType,
1272
+ ...action.parameters,
1273
+ };
1274
+
1275
+ // Preflight (always runs — validates frozen snapshot)
1276
+ const preflightResult = await adapter.preflight(actionInput, marketplaceContext);
1277
+ if (preflightResult.status === 'PREFLIGHT_FAILED') {
1278
+ actionResults.set(action.id, 'failed');
1279
+ await evidence.append({
1280
+ phase: 'reconcile-marketplace',
1281
+ actionId: action.id,
1282
+ actionType: action.type,
1283
+ decision: 'preflight-failed',
1284
+ error: preflightResult.error,
1285
+ });
1286
+ throw new ReleaseError(
1287
+ POST_PUBLISH_VERIFY_FAILED,
1288
+ `marketplace preflight failed for action "${action.id}": ${preflightResult.error}`,
1289
+ { actionId: action.id },
1290
+ );
1291
+ }
1292
+
1293
+ // Execute (install to isolated consumer directory)
1294
+ const executeResult = await adapter.execute(actionInput, marketplaceContext);
1295
+ if (executeResult.status !== 'EXECUTED') {
1296
+ actionResults.set(action.id, 'failed');
1297
+ await evidence.append({
1298
+ phase: 'reconcile-marketplace',
1299
+ actionId: action.id,
1300
+ actionType: action.type,
1301
+ decision: 'execute-failed',
1302
+ error: executeResult.error,
1303
+ });
1304
+ // Execute failed — if source was succeeded this is a conflict;
1305
+ // if source was failed/pending, add to the marketplace retry list
1306
+ if (sourceCp.status === 'succeeded') {
1307
+ throw new ReleaseError(
1308
+ REMOTE_CONFLICT,
1309
+ `marketplace execute failed for SUCCEEDED action "${action.id}": ${executeResult.error}`,
1310
+ { actionId: action.id },
1311
+ );
1312
+ }
1313
+ marketplaceRetriesToRetry.push(action);
1314
+ continue;
1315
+ }
1316
+
1317
+ // Verify (observe + match against plan expected state).
1318
+ // Retry the read-only verify while remote state is information-
1319
+ // insufficient (PROPAGATING, T1.1); a present mismatch is a
1320
+ // real conflict and is never retried.
1321
+ const verifyPolicy = clampPolicyToTimeout(
1322
+ DEFAULT_OBSERVE_RETRY_POLICY,
1323
+ action.parameters?.timeoutMs,
1324
+ );
1325
+ const verifyRetry = await observeWithRetry({
1326
+ observe: (act, ctx) => adapter.verify(act, ctx),
1327
+ action: { ...actionInput, expected: action.expected },
1328
+ context: marketplaceContext,
1329
+ policy: verifyPolicy,
1330
+ sleep: observeRetrySleep,
1331
+ onAttempt: (info) => evidence.append({
1332
+ phase: 'reconcile-marketplace-verify',
1333
+ actionId: action.id,
1334
+ actionType: action.type,
1335
+ attempt: info.attempt,
1336
+ maxAttempts: info.maxAttempts,
1337
+ missing: info.missing,
1338
+ delayMs: info.delayMs,
1339
+ status: info.missing ? 'propagating' : 'resolved',
1340
+ error: info.error ?? null,
1341
+ }),
1342
+ });
1343
+ const verifyResult = verifyRetry.result;
1344
+
1345
+ if (verifyResult.status === 'VERIFIED') {
1346
+ // Consistent: source succeeded => skipped, source failed => succeeded (recovered)
1347
+ const resultStatus = sourceCp.status === 'succeeded' ? 'skipped' : 'succeeded';
1348
+ actionResults.set(action.id, resultStatus);
1349
+ await evidence.append({
1350
+ phase: 'reconcile-marketplace',
1351
+ actionId: action.id,
1352
+ actionType: action.type,
1353
+ decision: sourceCp.status === 'succeeded' ? 'skip-source-succeeded-consistent' : 'recovered',
1354
+ sourceStatus: sourceCp.status,
1355
+ });
1356
+ } else {
1357
+ // Verify mismatch
1358
+ await evidence.append({
1359
+ phase: 'reconcile-marketplace',
1360
+ actionId: action.id,
1361
+ actionType: action.type,
1362
+ decision: 'verify-mismatch',
1363
+ error: verifyResult.error,
1364
+ });
1365
+ if (sourceCp.status === 'succeeded') {
1366
+ throw new ReleaseError(
1367
+ REMOTE_CONFLICT,
1368
+ `marketplace verify mismatch for SUCCEEDED action "${action.id}": ${verifyResult.error}`,
1369
+ { actionId: action.id },
1370
+ );
1371
+ }
1372
+ // Source was failed/pending and verify mismatched after recovery => failed.
1373
+ // Poisons ONLY the marketplace group; remote-write retries already ran.
1374
+ actionResults.set(action.id, 'failed');
1375
+ marketplaceRetryFailed = true;
1376
+ // Don't add to retry — this is a verify mismatch, not a missing state
1377
+ }
1378
+ }
1379
+
1380
+ // Marketplace retry candidates (isolated execute failed above) are
1381
+ // preflighted, then retried once through the shared journaled retry
1382
+ // loop. Gated ONLY on the marketplace group's own failure flag.
1383
+ if (!marketplaceRetryFailed && marketplaceRetriesToRetry.length > 0) {
1384
+ for (const action of marketplaceRetriesToRetry) {
1385
+ const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
1386
+ const adapter = adapterRegistry.getAdapter(adapterActionType);
1387
+ const preflightResult = await adapter.preflight(
1388
+ { actionType: adapterActionType, ...action.parameters },
1389
+ { ...context, externalWritesAuthorized: false, isolatedConsumerWritesAuthorized: true },
1390
+ );
1391
+ if (preflightResult.status === 'PREFLIGHT_FAILED') {
1392
+ // Mark all marketplace retry actions as failed, stop
1393
+ for (const retryAction of marketplaceRetriesToRetry) {
1394
+ actionResults.set(retryAction.id, 'failed');
1395
+ }
1396
+ marketplaceRetryFailed = true;
1397
+ await evidence.append({
1398
+ phase: 'reconcile-preflight',
1399
+ status: 'failed',
1400
+ actionId: action.id,
1401
+ error: preflightResult.error,
1402
+ });
1403
+ break;
1404
+ }
1405
+ }
1406
+ }
1407
+ if (!marketplaceRetryFailed && marketplaceRetriesToRetry.length > 0) {
1408
+ if (retryStateSequence < 0) {
1409
+ await persistRetryState('PUBLISHING');
1410
+ }
1411
+ await executeRetryGroup(marketplaceRetriesToRetry, () => { marketplaceRetryFailed = true; });
1243
1412
  }
1244
1413
 
1245
- // Mark remaining retry actions as pending if we stopped early
1246
- if (retryFailed) {
1414
+ // Mark remaining retry actions as pending if a group stopped early
1415
+ const markPendingAfterFailure = (groupActions) => {
1247
1416
  let foundFailed = false;
1248
- for (const action of actionsToRetry) {
1417
+ for (const action of groupActions) {
1249
1418
  if (['failed', 'uncertain'].includes(actionResults.get(action.id))) {
1250
1419
  foundFailed = true;
1251
1420
  continue;
@@ -1254,12 +1423,18 @@ export async function reconcileRelease(options) {
1254
1423
  actionResults.set(action.id, 'pending');
1255
1424
  }
1256
1425
  }
1426
+ };
1427
+ if (remoteWriteRetryFailed) {
1428
+ markPendingAfterFailure(actionsToRetry);
1429
+ }
1430
+ if (marketplaceRetryFailed) {
1431
+ markPendingAfterFailure(marketplaceRetriesToRetry);
1257
1432
  }
1258
1433
 
1259
1434
  // Final branch/default-branch consistency closes late drift after a retry
1260
1435
  // or after the first observation pass. The set-default action expectation
1261
1436
  // includes both the branch name and its exact planned commit.
1262
- if (!retryFailed && planActions.every((action) => ['succeeded', 'skipped'].includes(actionResults.get(action.id)))) {
1437
+ if (!remoteWriteRetryFailed && !marketplaceRetryFailed && planActions.every((action) => ['succeeded', 'skipped'].includes(actionResults.get(action.id)))) {
1263
1438
  await evidence.append({ phase: 'safety-gate', gate: 'final-branch-consistency', status: 'started' });
1264
1439
  for (const action of planActions) {
1265
1440
  if (!['push-snapshot', 'set-default-branch'].includes(action.type)) continue;
@@ -1279,7 +1454,7 @@ export async function reconcileRelease(options) {
1279
1454
  const comparison = observable ? matchObservation(action.expected ?? {}, observation) : { matches: false, mismatches: [] };
1280
1455
  if (!comparison.matches) {
1281
1456
  actionResults.set(action.id, observable ? 'failed' : 'uncertain');
1282
- retryFailed = true;
1457
+ remoteWriteRetryFailed = true;
1283
1458
  await evidence.append({
1284
1459
  phase: 'safety-gate',
1285
1460
  gate: 'final-branch-consistency',
@@ -1290,7 +1465,7 @@ export async function reconcileRelease(options) {
1290
1465
  break;
1291
1466
  }
1292
1467
  }
1293
- if (!retryFailed) {
1468
+ if (!remoteWriteRetryFailed) {
1294
1469
  await evidence.append({ phase: 'safety-gate', gate: 'final-branch-consistency', status: 'passed' });
1295
1470
  }
1296
1471
  }
@@ -1307,6 +1482,9 @@ export async function reconcileRelease(options) {
1307
1482
 
1308
1483
  const effectiveFromStatus = PARTIAL;
1309
1484
 
1485
+ // A failure in either independent retry group keeps the run PARTIAL.
1486
+ const retryFailed = remoteWriteRetryFailed || marketplaceRetryFailed;
1487
+
1310
1488
  // Map reconcile outcome to state machine target
1311
1489
  let overallStatus;
1312
1490
  if (allSucceeded && !retryFailed) {