release-skill 0.2.3 → 0.2.4
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 +22 -0
- package/CONTRIBUTING.md +27 -0
- package/INSTALL.md +95 -139
- package/INSTALL.zh-CN.md +70 -121
- package/README.md +264 -913
- package/README.zh-CN.md +222 -535
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/claude/schemas/release-plan.schema.json +137 -0
- package/adapters/claude/schemas/release-project.schema.json +93 -0
- package/adapters/claude/schemas/release-run.schema.json +70 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/codex/schemas/release-plan.schema.json +137 -0
- package/adapters/codex/schemas/release-project.schema.json +93 -0
- package/adapters/codex/schemas/release-run.schema.json +70 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/kimi/schemas/release-plan.schema.json +137 -0
- package/adapters/kimi/schemas/release-project.schema.json +93 -0
- package/adapters/kimi/schemas/release-run.schema.json +70 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +19054 -17573
- package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
- package/bin/release-skill.bundle.mjs +19054 -17573
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +137 -0
- package/schemas/release-project.schema.json +93 -0
- package/schemas/release-run.schema.json +70 -2
- package/src/adapters/plugin-marketplace.mjs +1190 -435
- package/src/commands/prepare.mjs +380 -40
- package/src/commands/publish.mjs +107 -75
- package/src/commands/reconcile.mjs +92 -327
- package/src/commands/setup.mjs +148 -20
- package/src/commands/verify.mjs +304 -20
- package/src/core/baseline.mjs +8 -1
- package/src/core/checkpoints.mjs +50 -7
- package/src/core/config.mjs +15 -0
- package/src/core/errors.mjs +2 -0
- package/src/core/installation-contract.mjs +341 -0
- package/src/core/plan.mjs +129 -6
- package/src/platforms/codebuddy.mjs +193 -280
- package/src/platforms/codex.mjs +369 -0
- package/src/platforms/kimi.mjs +164 -119
- package/src/platforms/registry.mjs +24 -6
|
@@ -39,7 +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
|
+
import { CHECKPOINT_ORDER, ADAPTER_ACTION_TYPE_MAP, isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
|
|
43
43
|
import {
|
|
44
44
|
loadRun,
|
|
45
45
|
validateRunPlanDigest,
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
GATE_FAILED,
|
|
57
57
|
BASELINE_CHANGED,
|
|
58
58
|
REMOTE_CONFLICT,
|
|
59
|
-
|
|
59
|
+
CONSUMER_VERIFICATION_DEFERRED,
|
|
60
60
|
} from '../core/errors.mjs';
|
|
61
61
|
import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-machine.mjs';
|
|
62
62
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
@@ -70,18 +70,6 @@ import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY }
|
|
|
70
70
|
// (single source shared with publish.mjs; T3.1 §4.7). The historical
|
|
71
71
|
// `Must match publish.mjs` double-write is removed.
|
|
72
72
|
|
|
73
|
-
/**
|
|
74
|
-
* Marketplace action types that are reconstructable isolated consumer checks,
|
|
75
|
-
* not permanent remote writes. These use isolatedConsumerWritesAuthorized
|
|
76
|
-
* and don't require externalWritesAuthorized even during retry.
|
|
77
|
-
*/
|
|
78
|
-
const MARKETPLACE_TYPES = new Set([
|
|
79
|
-
'claude-marketplace-install',
|
|
80
|
-
'codex-marketplace-install',
|
|
81
|
-
'kimi-marketplace-install',
|
|
82
|
-
'codebuddy-marketplace-install',
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
73
|
// ---------------------------------------------------------------------------
|
|
86
74
|
// Helpers
|
|
87
75
|
// ---------------------------------------------------------------------------
|
|
@@ -301,7 +289,7 @@ export async function reconcileRelease(options) {
|
|
|
301
289
|
const hasNonMarketplaceRetryCandidate = sourceRun.checkpoints.some((checkpoint) => {
|
|
302
290
|
if (checkpoint.status === 'succeeded') return false;
|
|
303
291
|
const action = planActionsById.get(checkpoint.actionId);
|
|
304
|
-
return action && !
|
|
292
|
+
return action && !isMarketplaceAction(action.type);
|
|
305
293
|
});
|
|
306
294
|
if (
|
|
307
295
|
plan.production?.mode === 'github-npm-v1' &&
|
|
@@ -407,7 +395,6 @@ export async function reconcileRelease(options) {
|
|
|
407
395
|
|
|
408
396
|
// Re-observe every unit's frozen public baseline before any adapter
|
|
409
397
|
// observation, preflight, or execute call.
|
|
410
|
-
const isProductionPlan = plan.production?.mode === 'github-npm-v1';
|
|
411
398
|
const defaultPpbObserveFn = async (repo, ref, expectedCommit, { githubHost = 'github.com' } = {}) => {
|
|
412
399
|
try {
|
|
413
400
|
const { execFile } = await import('node:child_process');
|
|
@@ -574,13 +561,8 @@ export async function reconcileRelease(options) {
|
|
|
574
561
|
// Non-marketplace actions use observe-based consistency checks.
|
|
575
562
|
// =======================================================================
|
|
576
563
|
const actionsToRetry = [];
|
|
577
|
-
const marketplaceRetriesToRetry = [];
|
|
578
564
|
const actionResults = new Map(); // actionId -> final status
|
|
579
|
-
// Remote-write retries and marketplace recovery are independent failure
|
|
580
|
-
// groups: neither group's failure blocks the other group's retries, and
|
|
581
|
-
// either failure keeps the run PARTIAL.
|
|
582
565
|
let remoteWriteRetryFailed = false;
|
|
583
|
-
let marketplaceRetryFailed = false;
|
|
584
566
|
|
|
585
567
|
for (const action of planActions) {
|
|
586
568
|
const sourceCp = sourceCpMap.get(action.id);
|
|
@@ -595,11 +577,13 @@ export async function reconcileRelease(options) {
|
|
|
595
577
|
const adapter = adapterRegistry.getAdapter(adapterActionType);
|
|
596
578
|
|
|
597
579
|
// -------------------------------------------------------------------
|
|
598
|
-
// Marketplace actions
|
|
599
|
-
//
|
|
580
|
+
// Marketplace actions are deferred to the verify phase. They are
|
|
581
|
+
// recorded as 'deferred' with CONSUMER_VERIFICATION_DEFERRED reason
|
|
582
|
+
// and never participate in reconcile's observe/retry loop.
|
|
600
583
|
// A marketplace failure must never block pending remote-write retries.
|
|
601
584
|
// -------------------------------------------------------------------
|
|
602
|
-
if (
|
|
585
|
+
if (isMarketplaceAction(action.type)) {
|
|
586
|
+
actionResults.set(action.id, 'deferred');
|
|
603
587
|
continue;
|
|
604
588
|
}
|
|
605
589
|
|
|
@@ -728,7 +712,7 @@ export async function reconcileRelease(options) {
|
|
|
728
712
|
|
|
729
713
|
// FAILED or PENDING: observe remote state (PROPAGATING retry, T1.1)
|
|
730
714
|
const observeInput = { actionType: adapterActionType, ...action.parameters };
|
|
731
|
-
const observePolicy =
|
|
715
|
+
const observePolicy = isMarketplaceAction(action.type)
|
|
732
716
|
? clampPolicyToTimeout(DEFAULT_OBSERVE_RETRY_POLICY, action.parameters?.timeoutMs)
|
|
733
717
|
: DEFAULT_OBSERVE_RETRY_POLICY;
|
|
734
718
|
const observeRetry = await observeWithRetry({
|
|
@@ -887,7 +871,7 @@ export async function reconcileRelease(options) {
|
|
|
887
871
|
// retries require approval.
|
|
888
872
|
// =======================================================================
|
|
889
873
|
// Phase 1 only collects non-marketplace retry candidates.
|
|
890
|
-
const nonMarketplaceRetries = actionsToRetry.filter((a) => !
|
|
874
|
+
const nonMarketplaceRetries = actionsToRetry.filter((a) => !isMarketplaceAction(a.type));
|
|
891
875
|
|
|
892
876
|
if (nonMarketplaceRetries.length > 0) {
|
|
893
877
|
// Non-marketplace retries require approval and productionConfirmation
|
|
@@ -926,19 +910,13 @@ export async function reconcileRelease(options) {
|
|
|
926
910
|
const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
|
|
927
911
|
const adapter = adapterRegistry.getAdapter(adapterActionType);
|
|
928
912
|
|
|
929
|
-
// Use appropriate context for each action type
|
|
930
|
-
const preflightCtx = MARKETPLACE_TYPES.has(action.type)
|
|
931
|
-
? { ...context, externalWritesAuthorized: false, isolatedConsumerWritesAuthorized: true }
|
|
932
|
-
: context;
|
|
933
|
-
|
|
934
913
|
const preflightResult = await adapter.preflight(
|
|
935
914
|
{ actionType: adapterActionType, ...action.parameters },
|
|
936
|
-
|
|
915
|
+
context,
|
|
937
916
|
);
|
|
938
917
|
|
|
939
918
|
if (preflightResult.status === 'PREFLIGHT_FAILED') {
|
|
940
|
-
// Mark all retry actions as failed, stop.
|
|
941
|
-
// remote-write group; marketplace recovery (phase 4) still runs.
|
|
919
|
+
// Mark all retry actions as failed, stop.
|
|
942
920
|
for (const retryAction of actionsToRetry) {
|
|
943
921
|
actionResults.set(retryAction.id, 'failed');
|
|
944
922
|
}
|
|
@@ -980,14 +958,17 @@ export async function reconcileRelease(options) {
|
|
|
980
958
|
status,
|
|
981
959
|
checkpoints: planActions.map((action) => {
|
|
982
960
|
const value = actionResults.get(action.id);
|
|
961
|
+
const normalized = value === 'deferred' ? 'deferred'
|
|
962
|
+
: value === 'succeeded' ? 'succeeded'
|
|
963
|
+
: value === 'skipped' ? 'skipped'
|
|
964
|
+
: value === 'failed' ? 'failed'
|
|
965
|
+
: value === 'uncertain' ? 'uncertain'
|
|
966
|
+
: 'pending';
|
|
983
967
|
return {
|
|
984
968
|
actionId: action.id,
|
|
985
969
|
actionType: action.type,
|
|
986
|
-
status:
|
|
987
|
-
|
|
988
|
-
: value === 'failed' ? 'failed'
|
|
989
|
-
: value === 'uncertain' ? 'uncertain'
|
|
990
|
-
: 'pending',
|
|
970
|
+
status: normalized,
|
|
971
|
+
...(normalized === 'deferred' ? { reason: CONSUMER_VERIFICATION_DEFERRED, phase: 'post-publish-verification' } : {}),
|
|
991
972
|
};
|
|
992
973
|
}),
|
|
993
974
|
startedAt: reconcileStartedAt,
|
|
@@ -1015,11 +996,9 @@ export async function reconcileRelease(options) {
|
|
|
1015
996
|
const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
|
|
1016
997
|
const adapter = adapterRegistry.getAdapter(adapterActionType);
|
|
1017
998
|
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
const retryCtx =
|
|
1021
|
-
? { ...context, externalWritesAuthorized: false, isolatedConsumerWritesAuthorized: true }
|
|
1022
|
-
: context;
|
|
999
|
+
// Only non-marketplace actions enter executeRetryGroup; marketplace
|
|
1000
|
+
// actions are deferred in Phase 1 and never reach this path.
|
|
1001
|
+
const retryCtx = context;
|
|
1023
1002
|
|
|
1024
1003
|
await evidence.append({
|
|
1025
1004
|
phase: 'reconcile-retry',
|
|
@@ -1045,60 +1024,12 @@ export async function reconcileRelease(options) {
|
|
|
1045
1024
|
}
|
|
1046
1025
|
|
|
1047
1026
|
if (executeResult.status === 'EXECUTED') {
|
|
1048
|
-
//
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const verifyInput = {
|
|
1055
|
-
actionType: adapterActionType,
|
|
1056
|
-
...action.parameters,
|
|
1057
|
-
expected: action.expected,
|
|
1058
|
-
};
|
|
1059
|
-
const verifyPolicy = clampPolicyToTimeout(
|
|
1060
|
-
DEFAULT_OBSERVE_RETRY_POLICY,
|
|
1061
|
-
action.parameters?.timeoutMs,
|
|
1062
|
-
);
|
|
1063
|
-
const verifyRetry = await observeWithRetry({
|
|
1064
|
-
observe: (act, ctx) => adapter.verify(act, ctx),
|
|
1065
|
-
action: verifyInput,
|
|
1066
|
-
context: retryCtx,
|
|
1067
|
-
policy: verifyPolicy,
|
|
1068
|
-
sleep: observeRetrySleep,
|
|
1069
|
-
onAttempt: (info) => evidence.append({
|
|
1070
|
-
phase: 'reconcile-retry-verify',
|
|
1071
|
-
actionId: action.id,
|
|
1072
|
-
actionType: action.type,
|
|
1073
|
-
attempt: info.attempt,
|
|
1074
|
-
maxAttempts: info.maxAttempts,
|
|
1075
|
-
missing: info.missing,
|
|
1076
|
-
delayMs: info.delayMs,
|
|
1077
|
-
status: info.missing ? 'propagating' : 'resolved',
|
|
1078
|
-
error: info.error ?? null,
|
|
1079
|
-
}),
|
|
1080
|
-
});
|
|
1081
|
-
const verifyResult = verifyRetry.result;
|
|
1082
|
-
|
|
1083
|
-
if (verifyResult.status !== 'VERIFIED') {
|
|
1084
|
-
actionResults.set(action.id, 'failed');
|
|
1085
|
-
markGroupFailed();
|
|
1086
|
-
await evidence.append({
|
|
1087
|
-
phase: 'reconcile-retry',
|
|
1088
|
-
actionId: action.id,
|
|
1089
|
-
actionType: action.type,
|
|
1090
|
-
status: 'verify-mismatch',
|
|
1091
|
-
error: verifyResult.error,
|
|
1092
|
-
});
|
|
1093
|
-
await persistRetryState(PARTIAL);
|
|
1094
|
-
break;
|
|
1095
|
-
}
|
|
1096
|
-
} else {
|
|
1097
|
-
// Non-marketplace: observe after execute to verify remote state.
|
|
1098
|
-
// execute succeeded, so a missing/uncertain observe may be a
|
|
1099
|
-
// transient propagation delay — retry with bounded backoff
|
|
1100
|
-
// (PROPAGATING, T1.1). A present mismatch is a real
|
|
1101
|
-
// conflict and is never retried.
|
|
1027
|
+
// Non-marketplace only: observe after execute to verify remote state.
|
|
1028
|
+
// execute succeeded, so a missing/uncertain observe may be a
|
|
1029
|
+
// transient propagation delay — retry with bounded backoff
|
|
1030
|
+
// (PROPAGATING, T1.1). A present mismatch is a real
|
|
1031
|
+
// conflict and is never retried.
|
|
1032
|
+
{
|
|
1102
1033
|
const observeInput = {
|
|
1103
1034
|
actionType: adapterActionType,
|
|
1104
1035
|
...action.parameters,
|
|
@@ -1186,39 +1117,35 @@ export async function reconcileRelease(options) {
|
|
|
1186
1117
|
// write. Non-marketplace actions must therefore be observed before
|
|
1187
1118
|
// classifying the checkpoint; unknown remains uncertain and an
|
|
1188
1119
|
// already-consistent remote is treated as succeeded.
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
observeResult = { observation: null, error: error.message };
|
|
1198
|
-
}
|
|
1199
|
-
const observation = observeResult?.observation;
|
|
1200
|
-
const missing = observation?.exists === false
|
|
1201
|
-
|| observation?.remoteCommit === ''
|
|
1202
|
-
|| observation?.commit === ''
|
|
1203
|
-
|| observation?.published === false;
|
|
1204
|
-
const matches = action.expected && observation
|
|
1205
|
-
? matchObservation(action.expected, observation).matches
|
|
1206
|
-
: false;
|
|
1207
|
-
if (matches) {
|
|
1208
|
-
actionResults.set(action.id, 'succeeded');
|
|
1209
|
-
await evidence.append({
|
|
1210
|
-
phase: 'reconcile-retry',
|
|
1211
|
-
actionId: action.id,
|
|
1212
|
-
actionType: action.type,
|
|
1213
|
-
status: 'completed-after-execute-failure-observe',
|
|
1214
|
-
});
|
|
1215
|
-
await persistRetryState(PARTIAL);
|
|
1216
|
-
continue;
|
|
1217
|
-
}
|
|
1218
|
-
actionResults.set(action.id, missing ? 'failed' : 'uncertain');
|
|
1219
|
-
} else {
|
|
1220
|
-
actionResults.set(action.id, 'failed');
|
|
1120
|
+
let observeResult;
|
|
1121
|
+
try {
|
|
1122
|
+
observeResult = await adapter.observe(
|
|
1123
|
+
{ actionType: adapterActionType, ...action.parameters, expected: action.expected },
|
|
1124
|
+
retryCtx,
|
|
1125
|
+
);
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
observeResult = { observation: null, error: error.message };
|
|
1221
1128
|
}
|
|
1129
|
+
const observation = observeResult?.observation;
|
|
1130
|
+
const missing = observation?.exists === false
|
|
1131
|
+
|| observation?.remoteCommit === ''
|
|
1132
|
+
|| observation?.commit === ''
|
|
1133
|
+
|| observation?.published === false;
|
|
1134
|
+
const matches = action.expected && observation
|
|
1135
|
+
? matchObservation(action.expected, observation).matches
|
|
1136
|
+
: false;
|
|
1137
|
+
if (matches) {
|
|
1138
|
+
actionResults.set(action.id, 'succeeded');
|
|
1139
|
+
await evidence.append({
|
|
1140
|
+
phase: 'reconcile-retry',
|
|
1141
|
+
actionId: action.id,
|
|
1142
|
+
actionType: action.type,
|
|
1143
|
+
status: 'completed-after-execute-failure-observe',
|
|
1144
|
+
});
|
|
1145
|
+
await persistRetryState(PARTIAL);
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
actionResults.set(action.id, missing ? 'failed' : 'uncertain');
|
|
1222
1149
|
markGroupFailed();
|
|
1223
1150
|
|
|
1224
1151
|
await evidence.append({
|
|
@@ -1244,174 +1171,6 @@ export async function reconcileRelease(options) {
|
|
|
1244
1171
|
await executeRetryGroup(actionsToRetry, () => { remoteWriteRetryFailed = true; });
|
|
1245
1172
|
}
|
|
1246
1173
|
|
|
1247
|
-
// --- Phase 4: Marketplace recovery (isolated consumer checks) ---
|
|
1248
|
-
//
|
|
1249
|
-
// Deferred until after the remote-write retry loop. Marketplace actions
|
|
1250
|
-
// are reconstructable isolated consumer checks, not permanent remote
|
|
1251
|
-
// writes: each gets a fresh preflight -> execute -> verify cycle in an
|
|
1252
|
-
// isolated directory, using isolatedConsumerWritesAuthorized instead of
|
|
1253
|
-
// externalWritesAuthorized. A failed remote-write retry must not
|
|
1254
|
-
// fabricate or suppress marketplace outcomes, and a marketplace failure
|
|
1255
|
-
// never blocks the remote-write group above.
|
|
1256
|
-
for (const action of planActions) {
|
|
1257
|
-
if (!MARKETPLACE_TYPES.has(action.type)) continue;
|
|
1258
|
-
const sourceCp = sourceCpMap.get(action.id);
|
|
1259
|
-
const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
|
|
1260
|
-
const adapter = adapterRegistry.getAdapter(adapterActionType);
|
|
1261
|
-
|
|
1262
|
-
const marketplaceContext = {
|
|
1263
|
-
externalWritesAuthorized: false,
|
|
1264
|
-
isolatedConsumerWritesAuthorized: true,
|
|
1265
|
-
plan,
|
|
1266
|
-
baseline: plan.baseline,
|
|
1267
|
-
root,
|
|
1268
|
-
runDir,
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
const actionInput = {
|
|
1272
|
-
actionType: adapterActionType,
|
|
1273
|
-
...action.parameters,
|
|
1274
|
-
};
|
|
1275
|
-
|
|
1276
|
-
// Preflight (always runs — validates frozen snapshot)
|
|
1277
|
-
const preflightResult = await adapter.preflight(actionInput, marketplaceContext);
|
|
1278
|
-
if (preflightResult.status === 'PREFLIGHT_FAILED') {
|
|
1279
|
-
actionResults.set(action.id, 'failed');
|
|
1280
|
-
await evidence.append({
|
|
1281
|
-
phase: 'reconcile-marketplace',
|
|
1282
|
-
actionId: action.id,
|
|
1283
|
-
actionType: action.type,
|
|
1284
|
-
decision: 'preflight-failed',
|
|
1285
|
-
error: preflightResult.error,
|
|
1286
|
-
});
|
|
1287
|
-
throw new ReleaseError(
|
|
1288
|
-
POST_PUBLISH_VERIFY_FAILED,
|
|
1289
|
-
`marketplace preflight failed for action "${action.id}": ${preflightResult.error}`,
|
|
1290
|
-
{ actionId: action.id },
|
|
1291
|
-
);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// Execute (install to isolated consumer directory)
|
|
1295
|
-
const executeResult = await adapter.execute(actionInput, marketplaceContext);
|
|
1296
|
-
if (executeResult.status !== 'EXECUTED') {
|
|
1297
|
-
actionResults.set(action.id, 'failed');
|
|
1298
|
-
await evidence.append({
|
|
1299
|
-
phase: 'reconcile-marketplace',
|
|
1300
|
-
actionId: action.id,
|
|
1301
|
-
actionType: action.type,
|
|
1302
|
-
decision: 'execute-failed',
|
|
1303
|
-
error: executeResult.error,
|
|
1304
|
-
});
|
|
1305
|
-
// Execute failed — if source was succeeded this is a conflict;
|
|
1306
|
-
// if source was failed/pending, add to the marketplace retry list
|
|
1307
|
-
if (sourceCp.status === 'succeeded') {
|
|
1308
|
-
throw new ReleaseError(
|
|
1309
|
-
REMOTE_CONFLICT,
|
|
1310
|
-
`marketplace execute failed for SUCCEEDED action "${action.id}": ${executeResult.error}`,
|
|
1311
|
-
{ actionId: action.id },
|
|
1312
|
-
);
|
|
1313
|
-
}
|
|
1314
|
-
marketplaceRetriesToRetry.push(action);
|
|
1315
|
-
continue;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Verify (observe + match against plan expected state).
|
|
1319
|
-
// Retry the read-only verify while remote state is information-
|
|
1320
|
-
// insufficient (PROPAGATING, T1.1); a present mismatch is a
|
|
1321
|
-
// real conflict and is never retried.
|
|
1322
|
-
const verifyPolicy = clampPolicyToTimeout(
|
|
1323
|
-
DEFAULT_OBSERVE_RETRY_POLICY,
|
|
1324
|
-
action.parameters?.timeoutMs,
|
|
1325
|
-
);
|
|
1326
|
-
const verifyRetry = await observeWithRetry({
|
|
1327
|
-
observe: (act, ctx) => adapter.verify(act, ctx),
|
|
1328
|
-
action: { ...actionInput, expected: action.expected },
|
|
1329
|
-
context: marketplaceContext,
|
|
1330
|
-
policy: verifyPolicy,
|
|
1331
|
-
sleep: observeRetrySleep,
|
|
1332
|
-
onAttempt: (info) => evidence.append({
|
|
1333
|
-
phase: 'reconcile-marketplace-verify',
|
|
1334
|
-
actionId: action.id,
|
|
1335
|
-
actionType: action.type,
|
|
1336
|
-
attempt: info.attempt,
|
|
1337
|
-
maxAttempts: info.maxAttempts,
|
|
1338
|
-
missing: info.missing,
|
|
1339
|
-
delayMs: info.delayMs,
|
|
1340
|
-
status: info.missing ? 'propagating' : 'resolved',
|
|
1341
|
-
error: info.error ?? null,
|
|
1342
|
-
}),
|
|
1343
|
-
});
|
|
1344
|
-
const verifyResult = verifyRetry.result;
|
|
1345
|
-
|
|
1346
|
-
if (verifyResult.status === 'VERIFIED') {
|
|
1347
|
-
// Consistent: source succeeded => skipped, source failed => succeeded (recovered)
|
|
1348
|
-
const resultStatus = sourceCp.status === 'succeeded' ? 'skipped' : 'succeeded';
|
|
1349
|
-
actionResults.set(action.id, resultStatus);
|
|
1350
|
-
await evidence.append({
|
|
1351
|
-
phase: 'reconcile-marketplace',
|
|
1352
|
-
actionId: action.id,
|
|
1353
|
-
actionType: action.type,
|
|
1354
|
-
decision: sourceCp.status === 'succeeded' ? 'skip-source-succeeded-consistent' : 'recovered',
|
|
1355
|
-
sourceStatus: sourceCp.status,
|
|
1356
|
-
});
|
|
1357
|
-
} else {
|
|
1358
|
-
// Verify mismatch
|
|
1359
|
-
await evidence.append({
|
|
1360
|
-
phase: 'reconcile-marketplace',
|
|
1361
|
-
actionId: action.id,
|
|
1362
|
-
actionType: action.type,
|
|
1363
|
-
decision: 'verify-mismatch',
|
|
1364
|
-
error: verifyResult.error,
|
|
1365
|
-
});
|
|
1366
|
-
if (sourceCp.status === 'succeeded') {
|
|
1367
|
-
throw new ReleaseError(
|
|
1368
|
-
REMOTE_CONFLICT,
|
|
1369
|
-
`marketplace verify mismatch for SUCCEEDED action "${action.id}": ${verifyResult.error}`,
|
|
1370
|
-
{ actionId: action.id },
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
// Source was failed/pending and verify mismatched after recovery => failed.
|
|
1374
|
-
// Poisons ONLY the marketplace group; remote-write retries already ran.
|
|
1375
|
-
actionResults.set(action.id, 'failed');
|
|
1376
|
-
marketplaceRetryFailed = true;
|
|
1377
|
-
// Don't add to retry — this is a verify mismatch, not a missing state
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Marketplace retry candidates (isolated execute failed above) are
|
|
1382
|
-
// preflighted, then retried once through the shared journaled retry
|
|
1383
|
-
// loop. Gated ONLY on the marketplace group's own failure flag.
|
|
1384
|
-
if (!marketplaceRetryFailed && marketplaceRetriesToRetry.length > 0) {
|
|
1385
|
-
for (const action of marketplaceRetriesToRetry) {
|
|
1386
|
-
const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
|
|
1387
|
-
const adapter = adapterRegistry.getAdapter(adapterActionType);
|
|
1388
|
-
const preflightResult = await adapter.preflight(
|
|
1389
|
-
{ actionType: adapterActionType, ...action.parameters },
|
|
1390
|
-
{ ...context, externalWritesAuthorized: false, isolatedConsumerWritesAuthorized: true },
|
|
1391
|
-
);
|
|
1392
|
-
if (preflightResult.status === 'PREFLIGHT_FAILED') {
|
|
1393
|
-
// Mark all marketplace retry actions as failed, stop
|
|
1394
|
-
for (const retryAction of marketplaceRetriesToRetry) {
|
|
1395
|
-
actionResults.set(retryAction.id, 'failed');
|
|
1396
|
-
}
|
|
1397
|
-
marketplaceRetryFailed = true;
|
|
1398
|
-
await evidence.append({
|
|
1399
|
-
phase: 'reconcile-preflight',
|
|
1400
|
-
status: 'failed',
|
|
1401
|
-
actionId: action.id,
|
|
1402
|
-
error: preflightResult.error,
|
|
1403
|
-
});
|
|
1404
|
-
break;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
if (!marketplaceRetryFailed && marketplaceRetriesToRetry.length > 0) {
|
|
1409
|
-
if (retryStateSequence < 0) {
|
|
1410
|
-
await persistRetryState('PUBLISHING');
|
|
1411
|
-
}
|
|
1412
|
-
await executeRetryGroup(marketplaceRetriesToRetry, () => { marketplaceRetryFailed = true; });
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
1174
|
// Mark remaining retry actions as pending if a group stopped early
|
|
1416
1175
|
const markPendingAfterFailure = (groupActions) => {
|
|
1417
1176
|
let foundFailed = false;
|
|
@@ -1428,14 +1187,11 @@ export async function reconcileRelease(options) {
|
|
|
1428
1187
|
if (remoteWriteRetryFailed) {
|
|
1429
1188
|
markPendingAfterFailure(actionsToRetry);
|
|
1430
1189
|
}
|
|
1431
|
-
if (marketplaceRetryFailed) {
|
|
1432
|
-
markPendingAfterFailure(marketplaceRetriesToRetry);
|
|
1433
|
-
}
|
|
1434
1190
|
|
|
1435
1191
|
// Final branch/default-branch consistency closes late drift after a retry
|
|
1436
1192
|
// or after the first observation pass. The set-default action expectation
|
|
1437
1193
|
// includes both the branch name and its exact planned commit.
|
|
1438
|
-
if (!remoteWriteRetryFailed &&
|
|
1194
|
+
if (!remoteWriteRetryFailed && planActions.filter((a) => isRemoteWriteAction(a.type)).every((action) => ['succeeded', 'skipped'].includes(actionResults.get(action.id)))) {
|
|
1439
1195
|
await evidence.append({ phase: 'safety-gate', gate: 'final-branch-consistency', status: 'started' });
|
|
1440
1196
|
for (const action of planActions) {
|
|
1441
1197
|
if (!['push-snapshot', 'set-default-branch'].includes(action.type)) continue;
|
|
@@ -1474,30 +1230,33 @@ export async function reconcileRelease(options) {
|
|
|
1474
1230
|
// =======================================================================
|
|
1475
1231
|
// Determine final status
|
|
1476
1232
|
// =======================================================================
|
|
1477
|
-
|
|
1478
|
-
|
|
1233
|
+
// 远端写入动作的状态决定整体发布状态
|
|
1234
|
+
// 市场安装动作的结果记录在 run 中,但不阻止 PUBLISHED
|
|
1235
|
+
const remoteWriteAllSucceeded = planActions
|
|
1236
|
+
.filter((a) => isRemoteWriteAction(a.type))
|
|
1237
|
+
.every((a) => {
|
|
1479
1238
|
const result = actionResults.get(a.id);
|
|
1480
1239
|
return result === 'succeeded' || result === 'skipped';
|
|
1481
|
-
}
|
|
1482
|
-
);
|
|
1240
|
+
});
|
|
1483
1241
|
|
|
1484
|
-
const
|
|
1242
|
+
const remoteWriteFailed = planActions
|
|
1243
|
+
.filter((a) => isRemoteWriteAction(a.type))
|
|
1244
|
+
.some((a) => {
|
|
1245
|
+
const result = actionResults.get(a.id);
|
|
1246
|
+
return result === 'failed' || result === 'uncertain';
|
|
1247
|
+
});
|
|
1485
1248
|
|
|
1486
|
-
|
|
1487
|
-
const retryFailed = remoteWriteRetryFailed || marketplaceRetryFailed;
|
|
1249
|
+
const effectiveFromStatus = PARTIAL;
|
|
1488
1250
|
|
|
1489
|
-
//
|
|
1251
|
+
// 远端写入动作的状态决定 PUBLISHED
|
|
1252
|
+
// 市场安装动作的失败不阻止 PUBLISHED
|
|
1490
1253
|
let overallStatus;
|
|
1491
|
-
if (
|
|
1254
|
+
if (remoteWriteAllSucceeded && !remoteWriteRetryFailed) {
|
|
1492
1255
|
overallStatus = PUBLISHED;
|
|
1493
|
-
} else if (
|
|
1494
|
-
// Once any retry execute was attempted, recovery must remain PARTIAL
|
|
1495
|
-
// even when no success was observed. BLOCKED is only pre-execute.
|
|
1256
|
+
} else if (remoteWriteFailed || remoteWriteRetryFailed) {
|
|
1496
1257
|
overallStatus = PARTIAL;
|
|
1497
1258
|
} else {
|
|
1498
|
-
|
|
1499
|
-
// belongs to the separate verify command.
|
|
1500
|
-
overallStatus = PUBLISHED;
|
|
1259
|
+
overallStatus = PARTIAL;
|
|
1501
1260
|
}
|
|
1502
1261
|
|
|
1503
1262
|
// Only validate state transition if status actually changes
|
|
@@ -1515,13 +1274,16 @@ export async function reconcileRelease(options) {
|
|
|
1515
1274
|
// Build checkpoints for return value and run file
|
|
1516
1275
|
const resultCheckpoints = planActions.map((a) => {
|
|
1517
1276
|
const status = actionResults.get(a.id) ?? 'pending';
|
|
1277
|
+
const normalized = status === 'deferred' ? 'deferred'
|
|
1278
|
+
: status === 'succeeded' ? 'succeeded'
|
|
1279
|
+
: status === 'failed' ? 'failed'
|
|
1280
|
+
: status === 'skipped' ? 'skipped'
|
|
1281
|
+
: status === 'uncertain' ? 'uncertain'
|
|
1282
|
+
: 'pending';
|
|
1518
1283
|
return {
|
|
1519
1284
|
actionId: a.id,
|
|
1520
|
-
status:
|
|
1521
|
-
|
|
1522
|
-
: status === 'skipped' ? 'skipped'
|
|
1523
|
-
: status === 'uncertain' ? 'uncertain'
|
|
1524
|
-
: 'pending',
|
|
1285
|
+
status: normalized,
|
|
1286
|
+
...(normalized === 'deferred' ? { reason: CONSUMER_VERIFICATION_DEFERRED, phase: 'post-publish-verification' } : {}),
|
|
1525
1287
|
};
|
|
1526
1288
|
});
|
|
1527
1289
|
|
|
@@ -1543,14 +1305,17 @@ export async function reconcileRelease(options) {
|
|
|
1543
1305
|
status: overallStatus,
|
|
1544
1306
|
checkpoints: planActions.map((a) => {
|
|
1545
1307
|
const status = actionResults.get(a.id) ?? 'pending';
|
|
1308
|
+
const normalized = status === 'deferred' ? 'deferred'
|
|
1309
|
+
: status === 'succeeded' ? 'succeeded'
|
|
1310
|
+
: status === 'failed' ? 'failed'
|
|
1311
|
+
: status === 'skipped' ? 'skipped'
|
|
1312
|
+
: status === 'uncertain' ? 'uncertain'
|
|
1313
|
+
: 'pending';
|
|
1546
1314
|
return {
|
|
1547
1315
|
actionId: a.id,
|
|
1548
1316
|
actionType: a.type,
|
|
1549
|
-
status:
|
|
1550
|
-
|
|
1551
|
-
: status === 'skipped' ? 'skipped'
|
|
1552
|
-
: status === 'uncertain' ? 'uncertain'
|
|
1553
|
-
: 'pending',
|
|
1317
|
+
status: normalized,
|
|
1318
|
+
...(normalized === 'deferred' ? { reason: CONSUMER_VERIFICATION_DEFERRED, phase: 'post-publish-verification' } : {}),
|
|
1554
1319
|
};
|
|
1555
1320
|
}),
|
|
1556
1321
|
startedAt: reconcileStartedAt,
|