mandrel 2.64.0 → 2.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/acceptance-critic.md +8 -7
- package/.agents/agents/auditor.md +20 -20
- package/.agents/agents/plan-critic.md +8 -7
- package/.agents/agents/story-worker.md +7 -7
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +1 -9
- package/.agents/docs/configuration.md +8 -7
- package/.agents/docs/execution-reference.md +27 -5
- package/.agents/instructions.md +10 -12
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +3 -2
- package/.agents/rules/git-conventions-reference.md +12 -3
- package/.agents/rules/git-conventions.md +9 -7
- package/.agents/rules/testing-standards.md +8 -7
- package/.agents/runtime-deps.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -13
- package/.agents/schemas/audit-rules.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
- package/.agents/scripts/bootstrap.js +102 -91
- package/.agents/scripts/check-context-budget.js +1 -1
- package/.agents/scripts/lib/ITicketingProvider.js +1 -3
- package/.agents/scripts/lib/audit-suite/findings.js +1 -17
- package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
- package/.agents/scripts/lib/audit-suite/index.js +0 -6
- package/.agents/scripts/lib/audit-suite/selector.js +0 -31
- package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
- package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
- package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
- package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +91 -107
- package/.agents/scripts/lib/cli/standard-args.js +60 -76
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/config/gates/shared.js +3 -3
- package/.agents/scripts/lib/config/review-chain-default.js +13 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
- package/.agents/scripts/lib/doc-tiers.js +25 -6
- package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
- package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
- package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
- package/.agents/scripts/lib/observability/signal-validator.js +17 -5
- package/.agents/scripts/lib/orchestration/code-review.js +33 -6
- package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
- package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
- package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
- package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
- package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +29 -4
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
- package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -12
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +370 -268
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +393 -313
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
- package/.agents/scripts/lib/signals/detectors/common.js +63 -51
- package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
- package/.agents/scripts/lib/transpile.js +28 -3
- package/.agents/scripts/providers/github/issues.js +14 -23
- package/.agents/scripts/single-story-close.js +10 -2
- package/.agents/scripts/single-story-confirm-merge.js +267 -238
- package/.agents/scripts/sync-claude-agents.js +1 -1
- package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
- package/.agents/workflows/audit-architecture.md +5 -4
- package/.agents/workflows/audit-documentation.md +5 -5
- package/.agents/workflows/audit-performance.md +10 -10
- package/.agents/workflows/audit-quality.md +42 -7
- package/.agents/workflows/helpers/acceptance-self-eval.md +9 -9
- package/.agents/workflows/helpers/audit-lens-core.md +30 -57
- package/.agents/workflows/helpers/code-review.md +15 -38
- package/.agents/workflows/helpers/deliver-digest.md +2 -2
- package/.agents/workflows/helpers/deliver-reference.md +7 -3
- package/.agents/workflows/helpers/deliver-story.md +9 -1
- package/.agents/workflows/helpers/parallel-tooling.md +16 -18
- package/.agents/workflows/helpers/plan-reference.md +9 -8
- package/.agents/workflows/mandrel-deliver.md +3 -2
- package/.agents/workflows/mandrel-plan.md +11 -7
- package/.agents/workflows/mandrel-update.md +5 -3
- package/docs/CHANGELOG.md +57 -0
- package/lib/cli/claude-code-version.js +73 -0
- package/lib/cli/doctor.js +2 -2
- package/lib/cli/guarded-sync.js +87 -0
- package/lib/cli/registry.js +9 -0
- package/lib/cli/sync-agents.js +9 -92
- package/lib/cli/sync-commands.js +9 -101
- package/lib/cli/uninstall.js +37 -9
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
- package/package.json +3 -2
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
- package/.agents/scripts/lib/audit-suite/runner.js +0 -205
- package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
- package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
|
@@ -44,14 +44,15 @@ import {
|
|
|
44
44
|
decideMergeWaitFailFast,
|
|
45
45
|
deriveChecksStatus,
|
|
46
46
|
deriveRedHeadRuns,
|
|
47
|
-
deriveRequiredRunEvidence,
|
|
48
47
|
isPrMerged,
|
|
49
48
|
MERGE_WAIT_GH_TIMEOUT_MS,
|
|
50
49
|
parseWorkflowRunId,
|
|
50
|
+
pollIntervalMs,
|
|
51
51
|
readRunSummary,
|
|
52
52
|
resolveAdvisoryGateVerdict,
|
|
53
53
|
} from '../../merge-poll.js';
|
|
54
54
|
import { readMergeQueueState } from '../../merge-queue.js';
|
|
55
|
+
import { readProbeRunEvidence } from '../../required-checks.js';
|
|
55
56
|
import { NEXT_COMMANDS } from '../../story-deliver-terminal.js';
|
|
56
57
|
import {
|
|
57
58
|
postStructuredComment,
|
|
@@ -155,6 +156,7 @@ export async function readPrWaitProbe({
|
|
|
155
156
|
gh = defaultGh,
|
|
156
157
|
ghTimeoutMs = MERGE_WAIT_GH_TIMEOUT_MS,
|
|
157
158
|
readMergeQueueStateFn = readMergeQueueState,
|
|
159
|
+
readRequiredCheckNamesFn,
|
|
158
160
|
}) {
|
|
159
161
|
try {
|
|
160
162
|
const view = await withGhTimeout(
|
|
@@ -188,7 +190,14 @@ export async function readPrWaitProbe({
|
|
|
188
190
|
}),
|
|
189
191
|
// Head-anchored, so superseded/pending runs don't read as red; `null`
|
|
190
192
|
// when the rollup is empty (the consecutive-probe fallback owns that).
|
|
191
|
-
requiredRunEvidence:
|
|
193
|
+
requiredRunEvidence: await readProbeRunEvidence({
|
|
194
|
+
view,
|
|
195
|
+
checksStatus,
|
|
196
|
+
prNumber,
|
|
197
|
+
gh,
|
|
198
|
+
ghTimeoutMs,
|
|
199
|
+
readFn: readRequiredCheckNamesFn,
|
|
200
|
+
}),
|
|
192
201
|
redHeadRuns: deriveRedHeadRuns(view?.statusCheckRollup),
|
|
193
202
|
headSha: readString(view?.headRefOid),
|
|
194
203
|
};
|
|
@@ -645,13 +654,11 @@ async function maybeRerunAdvisory({
|
|
|
645
654
|
}
|
|
646
655
|
|
|
647
656
|
/**
|
|
648
|
-
* Advisory-gate terminal for one poll
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* and disarms BEFORE returning a block so the PR cannot merge out from under it.
|
|
657
|
+
* Advisory-gate terminal for one poll, or `null` to keep polling. Skips runs
|
|
658
|
+
* already re-ran, enriches the rest, spends any rerun allowance, and disarms
|
|
659
|
+
* BEFORE returning a block so the PR cannot merge out from under it.
|
|
652
660
|
*/
|
|
653
661
|
async function resolveAdvisoryUnlanded({
|
|
654
|
-
unlanded,
|
|
655
662
|
probe,
|
|
656
663
|
blockOnAdvisoryFailure,
|
|
657
664
|
advisoryAllowlist,
|
|
@@ -663,7 +670,6 @@ async function resolveAdvisoryUnlanded({
|
|
|
663
670
|
disarmAutoMergeFn,
|
|
664
671
|
elapsedSeconds,
|
|
665
672
|
}) {
|
|
666
|
-
if (unlanded) return unlanded;
|
|
667
673
|
const advisory = decideAdvisoryGateBlock({
|
|
668
674
|
probe,
|
|
669
675
|
blockOnAdvisoryFailure,
|
|
@@ -992,6 +998,308 @@ async function onMergeObserved({
|
|
|
992
998
|
};
|
|
993
999
|
}
|
|
994
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Everything one poll hands the next.
|
|
1003
|
+
*
|
|
1004
|
+
* @param {{ startedAtMs: number, intervalSeconds: number }} args
|
|
1005
|
+
* @returns {{ startedAtMs: number, polls: number, intervalMs: number,
|
|
1006
|
+
* updatesUsed: number, consecutiveRequiredFailSnapshots: number }}
|
|
1007
|
+
*/
|
|
1008
|
+
export function createMergeWaitState({ startedAtMs, intervalSeconds }) {
|
|
1009
|
+
return {
|
|
1010
|
+
startedAtMs,
|
|
1011
|
+
polls: 0,
|
|
1012
|
+
intervalMs: intervalSeconds * 1000,
|
|
1013
|
+
updatesUsed: 0,
|
|
1014
|
+
consecutiveRequiredFailSnapshots: 0,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Budget first — it is the real give-up. */
|
|
1019
|
+
function provisionalVerdict(
|
|
1020
|
+
{ polls, intervalMs, waitedMs, cumulativeMs },
|
|
1021
|
+
limits,
|
|
1022
|
+
) {
|
|
1023
|
+
if (
|
|
1024
|
+
polls >= MIN_POLLS_BEFORE_BUDGET_BLOCK &&
|
|
1025
|
+
cumulativeMs + intervalMs > limits.maxBudgetSeconds * 1000
|
|
1026
|
+
) {
|
|
1027
|
+
return 'budget-exhausted';
|
|
1028
|
+
}
|
|
1029
|
+
if (waitedMs + intervalMs > limits.maxWaitSeconds * 1000) return 'wait-bound';
|
|
1030
|
+
return 'continue';
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Pure: state + probe + clock → next state and verdict. `merged`, `closed`
|
|
1035
|
+
* and `checks-failed` are definitive; `budget-exhausted`, `wait-bound` and
|
|
1036
|
+
* `continue` hold only if the loop's advisory check does not block first.
|
|
1037
|
+
* The two clock reads (wait bound, then budget) keep a stepping test clock's
|
|
1038
|
+
* call sequence.
|
|
1039
|
+
*
|
|
1040
|
+
* @param {{ state: object, probe: object, nowMs: number,
|
|
1041
|
+
* cumulativeNowMs?: number, limits: object }} args
|
|
1042
|
+
* @returns {{ state: object, verdict: string, waitedMs: number,
|
|
1043
|
+
* cumulativeMs: number, elapsedSeconds: number, waitBudget: object,
|
|
1044
|
+
* failFast?: object }}
|
|
1045
|
+
*/
|
|
1046
|
+
export function decideMergeWaitPoll({
|
|
1047
|
+
state,
|
|
1048
|
+
probe,
|
|
1049
|
+
nowMs,
|
|
1050
|
+
cumulativeNowMs = nowMs,
|
|
1051
|
+
limits,
|
|
1052
|
+
}) {
|
|
1053
|
+
const anchorMs = resolveBudgetAnchorMs({
|
|
1054
|
+
createdAt: probe.createdAt,
|
|
1055
|
+
fallbackMs: state.startedAtMs,
|
|
1056
|
+
});
|
|
1057
|
+
const waitedMs = nowMs - state.startedAtMs;
|
|
1058
|
+
const cumulativeMs = Math.max(cumulativeNowMs - anchorMs, waitedMs);
|
|
1059
|
+
const polled = {
|
|
1060
|
+
...state,
|
|
1061
|
+
polls: state.polls + 1,
|
|
1062
|
+
intervalMs: pollIntervalMs(probe.checksStatus, limits.intervalSeconds),
|
|
1063
|
+
};
|
|
1064
|
+
const timing = {
|
|
1065
|
+
waitedMs,
|
|
1066
|
+
cumulativeMs,
|
|
1067
|
+
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
1068
|
+
waitBudget: {
|
|
1069
|
+
maxWaitSeconds: limits.maxWaitSeconds,
|
|
1070
|
+
waitedSeconds: Math.round(waitedMs / 1000),
|
|
1071
|
+
cumulativeSeconds: Math.round(cumulativeMs / 1000),
|
|
1072
|
+
maxBudgetSeconds: limits.maxBudgetSeconds,
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
if (isPrMerged(probe)) return { state: polled, verdict: 'merged', ...timing };
|
|
1076
|
+
if (probe.state === 'CLOSED') {
|
|
1077
|
+
return { state: polled, verdict: 'closed', ...timing };
|
|
1078
|
+
}
|
|
1079
|
+
// No remaining budget turns a red required check green; fail fast.
|
|
1080
|
+
const failFast = decideMergeWaitFailFast({
|
|
1081
|
+
probe,
|
|
1082
|
+
consecutiveRequiredFailSnapshots: state.consecutiveRequiredFailSnapshots,
|
|
1083
|
+
});
|
|
1084
|
+
const next = {
|
|
1085
|
+
...polled,
|
|
1086
|
+
consecutiveRequiredFailSnapshots: failFast.consecutiveRequiredFailSnapshots,
|
|
1087
|
+
};
|
|
1088
|
+
if (failFast.failFast) {
|
|
1089
|
+
return { state: next, verdict: 'checks-failed', failFast, ...timing };
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
state: next,
|
|
1093
|
+
verdict: provisionalVerdict({ ...next, waitedMs, cumulativeMs }, limits),
|
|
1094
|
+
...timing,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function blockWith(ctx, unlanded) {
|
|
1099
|
+
return blockOnUnlanded({
|
|
1100
|
+
storyId: ctx.storyId,
|
|
1101
|
+
prNumber: ctx.prNumber,
|
|
1102
|
+
prUrl: ctx.prUrl,
|
|
1103
|
+
...unlanded,
|
|
1104
|
+
provider: ctx.provider,
|
|
1105
|
+
progress: ctx.progress,
|
|
1106
|
+
classifyMergeBlockFn: ctx.classifyMergeBlockFn,
|
|
1107
|
+
emitMergeUnlandedFn: ctx.emitMergeUnlandedFn,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function settleMerged(ctx, decision, probe) {
|
|
1112
|
+
return onMergeObserved({
|
|
1113
|
+
...ctx,
|
|
1114
|
+
prProbe: probe,
|
|
1115
|
+
elapsedSeconds: decision.elapsedSeconds,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function settleClosed(ctx, decision, probe) {
|
|
1120
|
+
return blockWith(ctx, {
|
|
1121
|
+
prProbe: probe,
|
|
1122
|
+
budget: { exhausted: true, elapsedSeconds: decision.elapsedSeconds },
|
|
1123
|
+
blockClassOverride: CLOSED_UNMERGED_BLOCK_CLASS,
|
|
1124
|
+
reasonOverride: CLOSED_UNMERGED_REASON,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async function settleChecksFailed(ctx, decision, probe) {
|
|
1129
|
+
const { failFast } = decision;
|
|
1130
|
+
ctx.progress?.(
|
|
1131
|
+
'CONFIRM',
|
|
1132
|
+
failFast.evidencePath === 'per-run'
|
|
1133
|
+
? `🛑 PR #${ctx.prNumber}: a required check concluded failure with none in flight — failing fast (evidence=per-run).`
|
|
1134
|
+
: `🛑 PR #${ctx.prNumber}: two consecutive failing check probes without per-run evidence — failing fast (evidence=consecutive-probe).`,
|
|
1135
|
+
);
|
|
1136
|
+
return blockWith(ctx, {
|
|
1137
|
+
prProbe: failFast.prProbe,
|
|
1138
|
+
budget: { exhausted: false, elapsedSeconds: decision.elapsedSeconds },
|
|
1139
|
+
blockClassOverride: failFast.blockClass,
|
|
1140
|
+
reasonOverride: failFast.reason,
|
|
1141
|
+
redRecord: await recordChecksFailedRed({
|
|
1142
|
+
storyId: ctx.storyId,
|
|
1143
|
+
prNumber: ctx.prNumber,
|
|
1144
|
+
probe,
|
|
1145
|
+
cwd: ctx.cwd,
|
|
1146
|
+
config: ctx.config,
|
|
1147
|
+
gh: ctx.injectedGh,
|
|
1148
|
+
progress: ctx.progress,
|
|
1149
|
+
disarmAutoMergeFn: ctx.disarmAutoMergeFn,
|
|
1150
|
+
recordRequiredRedFn: ctx.recordRequiredRedFn,
|
|
1151
|
+
}),
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const DEFINITIVE_SETTLERS = Object.freeze({
|
|
1156
|
+
merged: settleMerged,
|
|
1157
|
+
closed: settleClosed,
|
|
1158
|
+
'checks-failed': settleChecksFailed,
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
/** Invocation bound reached: resumable `pending`, no mutation. */
|
|
1162
|
+
function pendingAtWaitBound(ctx, decision, probe) {
|
|
1163
|
+
const { waitBudget } = decision;
|
|
1164
|
+
const checks = probe.checksStatus ?? 'unknown';
|
|
1165
|
+
ctx.progress?.(
|
|
1166
|
+
'CONFIRM',
|
|
1167
|
+
`⏸ Merge wait bound reached (${waitBudget.waitedSeconds}s of ${waitBudget.maxWaitSeconds}s this invocation; ` +
|
|
1168
|
+
`${waitBudget.cumulativeSeconds}s of ${waitBudget.maxBudgetSeconds}s cumulative). PR #${ctx.prNumber} still in flight ` +
|
|
1169
|
+
`(checks=${checks}). Story stays at agent::closing — resumable.`,
|
|
1170
|
+
);
|
|
1171
|
+
return {
|
|
1172
|
+
confirmed: false,
|
|
1173
|
+
terminal: 'pending',
|
|
1174
|
+
reason: `merge wait bound reached with the PR still in flight (checks=${checks})`,
|
|
1175
|
+
prProbe: probe,
|
|
1176
|
+
waitBudget,
|
|
1177
|
+
elapsedSeconds: waitBudget.waitedSeconds,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Advisory gates are usually still QUEUED at arm time, so they redden here,
|
|
1183
|
+
* mid-wait, before auto-merge lands over them.
|
|
1184
|
+
*
|
|
1185
|
+
* @returns {Promise<{ state: object, outcome: object|null }>}
|
|
1186
|
+
*/
|
|
1187
|
+
async function settleProvisional(ctx, decision, probe) {
|
|
1188
|
+
const advisory = await resolveAdvisoryUnlanded({
|
|
1189
|
+
probe,
|
|
1190
|
+
blockOnAdvisoryFailure: ctx.blockOnAdvisoryFailure,
|
|
1191
|
+
advisoryAllowlist: ctx.advisoryAllowlist,
|
|
1192
|
+
rerunState: ctx.rerunState,
|
|
1193
|
+
prNumber: ctx.prNumber,
|
|
1194
|
+
gh: ctx.injectedGh,
|
|
1195
|
+
ghTimeoutMs: ctx.ghTimeoutMs,
|
|
1196
|
+
progress: ctx.progress,
|
|
1197
|
+
disarmAutoMergeFn: ctx.disarmAutoMergeFn,
|
|
1198
|
+
elapsedSeconds: decision.elapsedSeconds,
|
|
1199
|
+
});
|
|
1200
|
+
if (advisory) {
|
|
1201
|
+
return { state: decision.state, outcome: await blockWith(ctx, advisory) };
|
|
1202
|
+
}
|
|
1203
|
+
const updated = await maybeUpdateBehindPr({
|
|
1204
|
+
probe,
|
|
1205
|
+
prNumber: ctx.prNumber,
|
|
1206
|
+
updatesUsed: decision.state.updatesUsed,
|
|
1207
|
+
updateAttempts: ctx.limits.updateAttempts,
|
|
1208
|
+
gh: ctx.injectedGh,
|
|
1209
|
+
ghTimeoutMs: ctx.ghTimeoutMs,
|
|
1210
|
+
progress: ctx.progress,
|
|
1211
|
+
});
|
|
1212
|
+
const state = updated
|
|
1213
|
+
? { ...decision.state, updatesUsed: decision.state.updatesUsed + 1 }
|
|
1214
|
+
: decision.state;
|
|
1215
|
+
if (decision.verdict === 'budget-exhausted') {
|
|
1216
|
+
const outcome = await blockWith(ctx, {
|
|
1217
|
+
prProbe: probe,
|
|
1218
|
+
budget: {
|
|
1219
|
+
exhausted: true,
|
|
1220
|
+
elapsedSeconds: Math.round(decision.cumulativeMs / 1000),
|
|
1221
|
+
},
|
|
1222
|
+
...queuedExhaustionVerdict(probe, decision.cumulativeMs),
|
|
1223
|
+
});
|
|
1224
|
+
return { state, outcome };
|
|
1225
|
+
}
|
|
1226
|
+
if (decision.verdict === 'wait-bound') {
|
|
1227
|
+
return { state, outcome: pendingAtWaitBound(ctx, decision, probe) };
|
|
1228
|
+
}
|
|
1229
|
+
return { state, outcome: null };
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** @returns {Promise<{ state: object, done: boolean, outcome?: object }>} */
|
|
1233
|
+
async function runMergePoll(ctx, state) {
|
|
1234
|
+
const probe = await ctx.readPrWaitProbeFn({
|
|
1235
|
+
prNumber: ctx.prNumber,
|
|
1236
|
+
gh: ctx.injectedGh,
|
|
1237
|
+
ghTimeoutMs: ctx.ghTimeoutMs,
|
|
1238
|
+
});
|
|
1239
|
+
const decision = decideMergeWaitPoll({
|
|
1240
|
+
state,
|
|
1241
|
+
probe,
|
|
1242
|
+
nowMs: ctx.nowMsFn(),
|
|
1243
|
+
cumulativeNowMs: ctx.nowMsFn(),
|
|
1244
|
+
limits: ctx.limits,
|
|
1245
|
+
});
|
|
1246
|
+
// Heartbeat: a backgrounded close's output-file growth is its liveness signal.
|
|
1247
|
+
ctx.progress?.(
|
|
1248
|
+
'CONFIRM',
|
|
1249
|
+
pollHeartbeat({
|
|
1250
|
+
polls: decision.state.polls,
|
|
1251
|
+
prNumber: ctx.prNumber,
|
|
1252
|
+
probe,
|
|
1253
|
+
waitBudget: decision.waitBudget,
|
|
1254
|
+
}),
|
|
1255
|
+
);
|
|
1256
|
+
const definitive = DEFINITIVE_SETTLERS[decision.verdict];
|
|
1257
|
+
const settled = definitive
|
|
1258
|
+
? {
|
|
1259
|
+
state: decision.state,
|
|
1260
|
+
outcome: await definitive(ctx, decision, probe),
|
|
1261
|
+
}
|
|
1262
|
+
: await settleProvisional(ctx, decision, probe);
|
|
1263
|
+
return settled.outcome
|
|
1264
|
+
? { state: settled.state, done: true, outcome: settled.outcome }
|
|
1265
|
+
: { state: settled.state, done: false };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/** Carries the arm phase's advisory class rather than a generic `arm-failure`. */
|
|
1269
|
+
function blockNeverArmed({
|
|
1270
|
+
storyId,
|
|
1271
|
+
prNumber,
|
|
1272
|
+
prUrl,
|
|
1273
|
+
autoMergeReason,
|
|
1274
|
+
advisoryGate,
|
|
1275
|
+
provider,
|
|
1276
|
+
progress,
|
|
1277
|
+
classifyMergeBlockFn,
|
|
1278
|
+
emitMergeUnlandedFn,
|
|
1279
|
+
}) {
|
|
1280
|
+
progress?.(
|
|
1281
|
+
'CONFIRM',
|
|
1282
|
+
`⚠️ Auto-merge not enabled (${autoMergeReason ?? 'unknown'}) — cannot wait for a merge that was never armed.`,
|
|
1283
|
+
);
|
|
1284
|
+
return blockOnUnlanded({
|
|
1285
|
+
storyId,
|
|
1286
|
+
prNumber,
|
|
1287
|
+
prUrl,
|
|
1288
|
+
armResult: { armed: false, reason: autoMergeReason },
|
|
1289
|
+
budget: { elapsedSeconds: 0 },
|
|
1290
|
+
provider,
|
|
1291
|
+
progress,
|
|
1292
|
+
classifyMergeBlockFn,
|
|
1293
|
+
emitMergeUnlandedFn,
|
|
1294
|
+
...(autoMergeReason === 'advisory-gate-red'
|
|
1295
|
+
? {
|
|
1296
|
+
blockClassOverride: advisoryGate?.blockClass ?? 'advisory-gate-red',
|
|
1297
|
+
reasonOverride: advisoryGate?.reason,
|
|
1298
|
+
}
|
|
1299
|
+
: {}),
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
|
|
995
1303
|
/**
|
|
996
1304
|
* Poll an armed Story PR to merge confirmation, a resumable `pending`
|
|
997
1305
|
* expiry, or a classified `agent::blocked` terminal.
|
|
@@ -1026,23 +1334,9 @@ async function onMergeObserved({
|
|
|
1026
1334
|
* @returns {Promise<object>}
|
|
1027
1335
|
*/
|
|
1028
1336
|
export async function runConfirmMergePhase({
|
|
1029
|
-
cwd,
|
|
1030
|
-
storyId,
|
|
1031
|
-
storyBranch,
|
|
1032
|
-
baseBranch,
|
|
1033
|
-
prNumber,
|
|
1034
|
-
prUrl,
|
|
1035
|
-
autoMergeEnabled,
|
|
1036
|
-
autoMergeReason,
|
|
1037
|
-
advisoryGate,
|
|
1038
|
-
provider,
|
|
1039
|
-
config,
|
|
1040
1337
|
maxWaitSeconds: maxWaitSecondsOverride,
|
|
1041
1338
|
mergeWatchMode: mergeWatchModeOverride,
|
|
1042
1339
|
rerunAdvisory: rerunAdvisoryOverride,
|
|
1043
|
-
progress,
|
|
1044
|
-
injectedGh,
|
|
1045
|
-
injectedNotify,
|
|
1046
1340
|
confirmStoryMergedFn = defaultConfirmStoryMerged,
|
|
1047
1341
|
readPrWaitProbeFn = readPrWaitProbe,
|
|
1048
1342
|
readPrMergeStateFn = defaultReadPrMergeState,
|
|
@@ -1055,282 +1349,90 @@ export async function runConfirmMergePhase({
|
|
|
1055
1349
|
sleepFn = defaultSleep,
|
|
1056
1350
|
nowMsFn = Date.now,
|
|
1057
1351
|
ghTimeoutMs = MERGE_WAIT_GH_TIMEOUT_MS,
|
|
1352
|
+
...args
|
|
1058
1353
|
}) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
'CONFIRM',
|
|
1063
|
-
`⚠️ Auto-merge not enabled (${autoMergeReason ?? 'unknown'}) — cannot wait for a merge that was never armed.`,
|
|
1064
|
-
);
|
|
1065
|
-
return blockOnUnlanded({
|
|
1066
|
-
storyId,
|
|
1067
|
-
prNumber,
|
|
1068
|
-
prUrl,
|
|
1069
|
-
armResult: { armed: false, reason: autoMergeReason },
|
|
1070
|
-
budget: { elapsedSeconds: 0 },
|
|
1071
|
-
provider,
|
|
1072
|
-
progress,
|
|
1354
|
+
if (!args.autoMergeEnabled) {
|
|
1355
|
+
return blockNeverArmed({
|
|
1356
|
+
...args,
|
|
1073
1357
|
classifyMergeBlockFn,
|
|
1074
1358
|
emitMergeUnlandedFn,
|
|
1075
|
-
// Carry the arm phase's advisory verdict and class (which may be
|
|
1076
|
-
// `inconclusive`) rather than a generic `arm-failure`.
|
|
1077
|
-
...(autoMergeReason === 'advisory-gate-red'
|
|
1078
|
-
? {
|
|
1079
|
-
blockClassOverride: advisoryGate?.blockClass ?? 'advisory-gate-red',
|
|
1080
|
-
reasonOverride: advisoryGate?.reason,
|
|
1081
|
-
}
|
|
1082
|
-
: {}),
|
|
1083
1359
|
});
|
|
1084
1360
|
}
|
|
1085
1361
|
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
intervalSeconds,
|
|
1089
|
-
maxWaitSeconds,
|
|
1090
|
-
maxBudgetSeconds,
|
|
1091
|
-
updateAttempts,
|
|
1092
|
-
} = resolveMergeWaitConfig(
|
|
1093
|
-
config,
|
|
1362
|
+
const limits = resolveMergeWaitConfig(
|
|
1363
|
+
args.config,
|
|
1094
1364
|
maxWaitSecondsOverride,
|
|
1095
1365
|
mergeWatchModeOverride,
|
|
1096
1366
|
);
|
|
1097
|
-
const { blockOnAdvisoryFailure, advisoryAllowlist } = getCiDelivery(
|
|
1367
|
+
const { blockOnAdvisoryFailure, advisoryAllowlist } = getCiDelivery(
|
|
1368
|
+
args.config,
|
|
1369
|
+
);
|
|
1098
1370
|
// Spent across the whole wait, not per poll, so `n` bounds CI minutes.
|
|
1099
1371
|
const rerunAllowance = resolveAdvisoryRerunAllowance(
|
|
1100
|
-
config,
|
|
1372
|
+
args.config,
|
|
1101
1373
|
rerunAdvisoryOverride,
|
|
1102
1374
|
);
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1375
|
+
const ctx = {
|
|
1376
|
+
...args,
|
|
1377
|
+
limits,
|
|
1378
|
+
blockOnAdvisoryFailure,
|
|
1379
|
+
advisoryAllowlist,
|
|
1380
|
+
rerunState: {
|
|
1381
|
+
allowance: rerunAllowance,
|
|
1382
|
+
remaining: rerunAllowance,
|
|
1383
|
+
issued: new Set(),
|
|
1384
|
+
},
|
|
1385
|
+
confirmStoryMergedFn,
|
|
1386
|
+
readPrWaitProbeFn,
|
|
1387
|
+
readPrMergeStateFn,
|
|
1388
|
+
classifyMergeBlockFn,
|
|
1389
|
+
emitMergeUnlandedFn,
|
|
1390
|
+
emitMergeFlipFailedFn,
|
|
1391
|
+
runPostLandTailFn,
|
|
1392
|
+
disarmAutoMergeFn,
|
|
1393
|
+
recordRequiredRedFn,
|
|
1394
|
+
nowMsFn,
|
|
1395
|
+
ghTimeoutMs,
|
|
1107
1396
|
};
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
let polls = 0;
|
|
1113
|
-
// Without per-run evidence, fail fast only after two consecutive failing
|
|
1114
|
-
// probes an interval apart; reset on any other probe.
|
|
1115
|
-
let consecutiveRequiredFailSnapshots = 0;
|
|
1397
|
+
let state = createMergeWaitState({
|
|
1398
|
+
startedAtMs: nowMsFn(),
|
|
1399
|
+
intervalSeconds: limits.intervalSeconds,
|
|
1400
|
+
});
|
|
1116
1401
|
|
|
1117
|
-
progress?.(
|
|
1402
|
+
args.progress?.(
|
|
1118
1403
|
'CONFIRM',
|
|
1119
|
-
`⏳ Close-and-land: polling PR #${prNumber} for merge confirmation ` +
|
|
1120
|
-
`(mode=${mode}, wait=${maxWaitSeconds}s this invocation, ` +
|
|
1121
|
-
`cumulative budget=${maxBudgetSeconds}s)...`,
|
|
1404
|
+
`⏳ Close-and-land: polling PR #${args.prNumber} for merge confirmation ` +
|
|
1405
|
+
`(mode=${limits.mode}, wait=${limits.maxWaitSeconds}s this invocation, ` +
|
|
1406
|
+
`cumulative budget=${limits.maxBudgetSeconds}s)...`,
|
|
1122
1407
|
);
|
|
1123
1408
|
|
|
1124
|
-
/** One iteration: `{ done: false }` or `{ done: true, outcome }`. */
|
|
1125
|
-
async function runMergePoll() {
|
|
1126
|
-
const probe = await readPrWaitProbeFn({
|
|
1127
|
-
prNumber,
|
|
1128
|
-
gh: injectedGh,
|
|
1129
|
-
ghTimeoutMs,
|
|
1130
|
-
});
|
|
1131
|
-
polls += 1;
|
|
1132
|
-
|
|
1133
|
-
anchorMs = resolveBudgetAnchorMs({
|
|
1134
|
-
createdAt: probe.createdAt,
|
|
1135
|
-
fallbackMs: startedAtMs,
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
const waitedMs = nowMsFn() - startedAtMs;
|
|
1139
|
-
const cumulativeMs = Math.max(nowMsFn() - anchorMs, waitedMs);
|
|
1140
|
-
const waitBudget = {
|
|
1141
|
-
maxWaitSeconds,
|
|
1142
|
-
waitedSeconds: Math.round(waitedMs / 1000),
|
|
1143
|
-
cumulativeSeconds: Math.round(cumulativeMs / 1000),
|
|
1144
|
-
maxBudgetSeconds,
|
|
1145
|
-
};
|
|
1146
|
-
|
|
1147
|
-
// Heartbeat: a backgrounded close's output-file growth is its liveness signal.
|
|
1148
|
-
progress?.(
|
|
1149
|
-
'CONFIRM',
|
|
1150
|
-
`⏱ poll ${polls}: PR #${prNumber} state=${probe.state ?? 'unknown'} ` +
|
|
1151
|
-
`checks=${probe.checksStatus ?? 'unknown'} ` +
|
|
1152
|
-
`mergeState=${probe.mergeStateStatus ?? 'unknown'} ` +
|
|
1153
|
-
`(${waitBudget.waitedSeconds}s of ${maxWaitSeconds}s this invocation; ` +
|
|
1154
|
-
`${waitBudget.cumulativeSeconds}s of ${maxBudgetSeconds}s cumulative)` +
|
|
1155
|
-
(probe.error ? ` — probe error: ${probe.error}` : ''),
|
|
1156
|
-
);
|
|
1157
|
-
|
|
1158
|
-
if (isPrMerged(probe)) {
|
|
1159
|
-
return doneWith(
|
|
1160
|
-
await onMergeObserved({
|
|
1161
|
-
storyId,
|
|
1162
|
-
storyBranch,
|
|
1163
|
-
baseBranch,
|
|
1164
|
-
prNumber,
|
|
1165
|
-
prUrl,
|
|
1166
|
-
cwd,
|
|
1167
|
-
config,
|
|
1168
|
-
provider,
|
|
1169
|
-
progress,
|
|
1170
|
-
injectedGh,
|
|
1171
|
-
injectedNotify,
|
|
1172
|
-
readPrMergeStateFn,
|
|
1173
|
-
confirmStoryMergedFn,
|
|
1174
|
-
runPostLandTailFn,
|
|
1175
|
-
emitMergeFlipFailedFn,
|
|
1176
|
-
prProbe: probe,
|
|
1177
|
-
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
1178
|
-
}),
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// Each definitive condition fills `unlanded`; one call site below blocks.
|
|
1183
|
-
let unlanded = null;
|
|
1184
|
-
|
|
1185
|
-
if (probe.state === 'CLOSED') {
|
|
1186
|
-
unlanded = {
|
|
1187
|
-
prProbe: probe,
|
|
1188
|
-
budget: {
|
|
1189
|
-
exhausted: true,
|
|
1190
|
-
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
1191
|
-
},
|
|
1192
|
-
blockClassOverride: CLOSED_UNMERGED_BLOCK_CLASS,
|
|
1193
|
-
reasonOverride: CLOSED_UNMERGED_REASON,
|
|
1194
|
-
};
|
|
1195
|
-
} else {
|
|
1196
|
-
// No remaining budget turns a red required check green; fail fast.
|
|
1197
|
-
const decision = decideMergeWaitFailFast({
|
|
1198
|
-
probe,
|
|
1199
|
-
consecutiveRequiredFailSnapshots,
|
|
1200
|
-
});
|
|
1201
|
-
consecutiveRequiredFailSnapshots =
|
|
1202
|
-
decision.consecutiveRequiredFailSnapshots;
|
|
1203
|
-
if (decision.failFast) {
|
|
1204
|
-
progress?.(
|
|
1205
|
-
'CONFIRM',
|
|
1206
|
-
decision.evidencePath === 'per-run'
|
|
1207
|
-
? `🛑 PR #${prNumber}: a required check concluded failure with none in flight — failing fast (evidence=per-run).`
|
|
1208
|
-
: `🛑 PR #${prNumber}: two consecutive failing check probes without per-run evidence — failing fast (evidence=consecutive-probe).`,
|
|
1209
|
-
);
|
|
1210
|
-
unlanded = {
|
|
1211
|
-
prProbe: decision.prProbe,
|
|
1212
|
-
budget: {
|
|
1213
|
-
exhausted: false,
|
|
1214
|
-
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
1215
|
-
},
|
|
1216
|
-
blockClassOverride: decision.blockClass,
|
|
1217
|
-
reasonOverride: decision.reason,
|
|
1218
|
-
redRecord: await recordChecksFailedRed({
|
|
1219
|
-
storyId,
|
|
1220
|
-
prNumber,
|
|
1221
|
-
probe,
|
|
1222
|
-
cwd,
|
|
1223
|
-
config,
|
|
1224
|
-
gh: injectedGh,
|
|
1225
|
-
progress,
|
|
1226
|
-
disarmAutoMergeFn,
|
|
1227
|
-
recordRequiredRedFn,
|
|
1228
|
-
}),
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Advisory gates are usually still QUEUED at arm time, so they redden
|
|
1233
|
-
// here, mid-wait, before auto-merge lands over them.
|
|
1234
|
-
unlanded = await resolveAdvisoryUnlanded({
|
|
1235
|
-
unlanded,
|
|
1236
|
-
probe,
|
|
1237
|
-
blockOnAdvisoryFailure,
|
|
1238
|
-
advisoryAllowlist,
|
|
1239
|
-
rerunState,
|
|
1240
|
-
prNumber,
|
|
1241
|
-
gh: injectedGh,
|
|
1242
|
-
ghTimeoutMs,
|
|
1243
|
-
progress,
|
|
1244
|
-
disarmAutoMergeFn,
|
|
1245
|
-
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
if (!unlanded) {
|
|
1250
|
-
if (
|
|
1251
|
-
await maybeUpdateBehindPr({
|
|
1252
|
-
probe,
|
|
1253
|
-
prNumber,
|
|
1254
|
-
updatesUsed,
|
|
1255
|
-
updateAttempts,
|
|
1256
|
-
gh: injectedGh,
|
|
1257
|
-
ghTimeoutMs,
|
|
1258
|
-
progress,
|
|
1259
|
-
})
|
|
1260
|
-
) {
|
|
1261
|
-
updatesUsed += 1;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
// Cumulative budget exhausted, behind the poll floor.
|
|
1265
|
-
if (
|
|
1266
|
-
polls >= MIN_POLLS_BEFORE_BUDGET_BLOCK &&
|
|
1267
|
-
cumulativeMs + intervalMs > maxBudgetSeconds * 1000
|
|
1268
|
-
) {
|
|
1269
|
-
unlanded = {
|
|
1270
|
-
prProbe: probe,
|
|
1271
|
-
budget: {
|
|
1272
|
-
exhausted: true,
|
|
1273
|
-
elapsedSeconds: Math.round(cumulativeMs / 1000),
|
|
1274
|
-
},
|
|
1275
|
-
...queuedExhaustionVerdict(probe, cumulativeMs),
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (unlanded) {
|
|
1281
|
-
return doneWith(
|
|
1282
|
-
await blockOnUnlanded({
|
|
1283
|
-
storyId,
|
|
1284
|
-
prNumber,
|
|
1285
|
-
prUrl,
|
|
1286
|
-
...unlanded,
|
|
1287
|
-
provider,
|
|
1288
|
-
progress,
|
|
1289
|
-
classifyMergeBlockFn,
|
|
1290
|
-
emitMergeUnlandedFn,
|
|
1291
|
-
}),
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// Invocation bound reached: resumable `pending`, no mutation.
|
|
1296
|
-
if (waitedMs + intervalMs > maxWaitSeconds * 1000) {
|
|
1297
|
-
progress?.(
|
|
1298
|
-
'CONFIRM',
|
|
1299
|
-
`⏸ Merge wait bound reached (${waitBudget.waitedSeconds}s of ${maxWaitSeconds}s this invocation; ` +
|
|
1300
|
-
`${waitBudget.cumulativeSeconds}s of ${maxBudgetSeconds}s cumulative). PR #${prNumber} still in flight ` +
|
|
1301
|
-
`(checks=${probe.checksStatus ?? 'unknown'}). Story stays at agent::closing — resumable.`,
|
|
1302
|
-
);
|
|
1303
|
-
return doneWith({
|
|
1304
|
-
confirmed: false,
|
|
1305
|
-
terminal: 'pending',
|
|
1306
|
-
reason: `merge wait bound reached with the PR still in flight (checks=${probe.checksStatus ?? 'unknown'})`,
|
|
1307
|
-
prProbe: probe,
|
|
1308
|
-
waitBudget,
|
|
1309
|
-
elapsedSeconds: waitBudget.waitedSeconds,
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
return { done: false };
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
1409
|
const tick = await pollUntil({
|
|
1317
1410
|
fn: async () => {
|
|
1318
1411
|
try {
|
|
1319
|
-
|
|
1412
|
+
const step = await runMergePoll(ctx, state);
|
|
1413
|
+
state = step.state;
|
|
1414
|
+
return step;
|
|
1320
1415
|
} catch (err) {
|
|
1321
1416
|
// pollUntil treats a throw as a non-match and would spin; carry it out.
|
|
1322
1417
|
return { done: true, thrown: err };
|
|
1323
1418
|
}
|
|
1324
1419
|
},
|
|
1325
1420
|
predicate: (result) => result?.done === true,
|
|
1326
|
-
intervalMs,
|
|
1327
|
-
// No pollUntil timeout: the tick owns both bounds and
|
|
1328
|
-
sleepFn: (
|
|
1421
|
+
intervalMs: state.intervalMs,
|
|
1422
|
+
// No pollUntil timeout: the tick owns both bounds and the cadence.
|
|
1423
|
+
sleepFn: () => sleepFn(state.intervalMs),
|
|
1329
1424
|
});
|
|
1330
1425
|
if (tick.thrown) throw tick.thrown;
|
|
1331
1426
|
return tick.outcome;
|
|
1332
1427
|
}
|
|
1333
1428
|
|
|
1334
|
-
function
|
|
1335
|
-
return
|
|
1429
|
+
function pollHeartbeat({ polls, prNumber, probe, waitBudget }) {
|
|
1430
|
+
return (
|
|
1431
|
+
`⏱ poll ${polls}: PR #${prNumber} state=${probe.state ?? 'unknown'} ` +
|
|
1432
|
+
`checks=${probe.checksStatus ?? 'unknown'} ` +
|
|
1433
|
+
`mergeState=${probe.mergeStateStatus ?? 'unknown'} ` +
|
|
1434
|
+
`(${waitBudget.waitedSeconds}s of ${waitBudget.maxWaitSeconds}s this invocation; ` +
|
|
1435
|
+
`${waitBudget.cumulativeSeconds}s of ${waitBudget.maxBudgetSeconds}s cumulative)` +
|
|
1436
|
+
(probe.error ? ` — probe error: ${probe.error}` : '')
|
|
1437
|
+
);
|
|
1336
1438
|
}
|