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
|
@@ -9,6 +9,11 @@ import { getCiDelivery } from '../../config/ci.js';
|
|
|
9
9
|
import { resolveConfig } from '../../config-resolver.js';
|
|
10
10
|
import { getStoryBranch, gitSpawn, gitSync } from '../../git-utils.js';
|
|
11
11
|
import { Logger } from '../../Logger.js';
|
|
12
|
+
import {
|
|
13
|
+
emitReviewBlockedFriction,
|
|
14
|
+
recordCloseTelemetry,
|
|
15
|
+
resolveWorkerTokens,
|
|
16
|
+
} from '../../observability/close-telemetry.js';
|
|
12
17
|
import { emitTerminalFriction } from '../../observability/runtime-friction.js';
|
|
13
18
|
import { emitTerseResult } from '../../observability/terse-result.js';
|
|
14
19
|
import { createProvider } from '../../provider-factory.js';
|
|
@@ -33,6 +38,7 @@ import { runConfirmMergePhase } from './phases/confirm-merge.js';
|
|
|
33
38
|
import { runGraphqlPreflight } from './phases/graphql-preflight.js';
|
|
34
39
|
import { lockWaitPending } from './phases/lock-wait-pending.js';
|
|
35
40
|
import { parseCloseOptions, resolveWaitForMerge } from './phases/options.js';
|
|
41
|
+
import { runPostLandTail } from './phases/post-land.js';
|
|
36
42
|
import { ensurePullRequestWith } from './phases/pull-request.js';
|
|
37
43
|
import { pushStoryBranch } from './phases/push.js';
|
|
38
44
|
import { handleCriticalReviewBlock } from './phases/review-block.js';
|
|
@@ -43,11 +49,56 @@ import { runWrongTreeGuardPhase } from './phases/wrong-tree-guard.js';
|
|
|
43
49
|
const progress = Logger.createProgress('single-story-close', { stderr: true });
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
52
|
+
* Wall-clock seconds per named phase; each transition logs the phase it ends.
|
|
53
|
+
*
|
|
54
|
+
* @param {() => number} [nowMs]
|
|
49
55
|
*/
|
|
50
|
-
|
|
56
|
+
function createPhaseTimer(nowMs = Date.now) {
|
|
57
|
+
const durations = {};
|
|
58
|
+
let current = null;
|
|
59
|
+
let since = 0;
|
|
60
|
+
const end = () => {
|
|
61
|
+
if (current === null) return;
|
|
62
|
+
const seconds = Math.round((nowMs() - since) / 100) / 10;
|
|
63
|
+
durations[current] = (durations[current] ?? 0) + seconds;
|
|
64
|
+
progress('TIMING', `⏱ ${current}: ${seconds}s`);
|
|
65
|
+
current = null;
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
enter(phase) {
|
|
69
|
+
end();
|
|
70
|
+
if (phase === 'init') return;
|
|
71
|
+
current = phase;
|
|
72
|
+
since = nowMs();
|
|
73
|
+
},
|
|
74
|
+
finish() {
|
|
75
|
+
end();
|
|
76
|
+
return Object.keys(durations).length > 0 ? { ...durations } : null;
|
|
77
|
+
},
|
|
78
|
+
stamp(terminal) {
|
|
79
|
+
const phaseDurations = this.finish();
|
|
80
|
+
if (phaseDurations) terminal.phaseDurations = phaseDurations;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const UNTIMED = Object.freeze({ stamp() {} });
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The single terminal writer: the retry signal, the result summary (with its
|
|
89
|
+
* `telemetry`), the envelope callers parse, and terminal friction — so no
|
|
90
|
+
* ending can forget one. Must be awaited: the CLI `process.exit`s as soon as
|
|
91
|
+
* `main` resolves.
|
|
92
|
+
*/
|
|
93
|
+
async function emitTerminal({
|
|
94
|
+
terminal,
|
|
95
|
+
result,
|
|
96
|
+
config,
|
|
97
|
+
phaseTimer = UNTIMED,
|
|
98
|
+
workerTokens = null,
|
|
99
|
+
}) {
|
|
100
|
+
phaseTimer.stamp(terminal);
|
|
101
|
+
await recordCloseTelemetry({ terminal, result, config, workerTokens });
|
|
51
102
|
if (result) {
|
|
52
103
|
emitTerseResult({
|
|
53
104
|
label: 'STORY CLOSE RESULT',
|
|
@@ -66,23 +117,32 @@ async function emitTerminal({ terminal, result, config }) {
|
|
|
66
117
|
await emitTerminalFriction({ envelope: terminal, config });
|
|
67
118
|
}
|
|
68
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @param {number} storyId
|
|
122
|
+
* @param {string} reason
|
|
123
|
+
* @returns {{ storyId: number, standalone: true, action: 'noop', reason: string }}
|
|
124
|
+
*/
|
|
125
|
+
function noopResult(storyId, reason) {
|
|
126
|
+
return { storyId, standalone: true, action: 'noop', reason };
|
|
127
|
+
}
|
|
128
|
+
|
|
69
129
|
/**
|
|
70
130
|
* Terminal for an already-closed Story. `not_planned` means nothing merged,
|
|
71
131
|
* so it fails rather than reporting `landed` (which would also unblock
|
|
72
132
|
* dependents); `completed` or null (GitHub's default) reads as landed.
|
|
73
133
|
*/
|
|
74
|
-
async function alreadyClosedResult(
|
|
134
|
+
async function alreadyClosedResult(
|
|
135
|
+
storyId,
|
|
136
|
+
stateReason = null,
|
|
137
|
+
config,
|
|
138
|
+
workerTokens = null,
|
|
139
|
+
) {
|
|
75
140
|
if (stateReason === 'not_planned') {
|
|
76
141
|
progress(
|
|
77
142
|
'NOOP',
|
|
78
143
|
`Story #${storyId} is closed as not planned — nothing to land.`,
|
|
79
144
|
);
|
|
80
|
-
const result =
|
|
81
|
-
storyId,
|
|
82
|
-
standalone: true,
|
|
83
|
-
action: 'noop',
|
|
84
|
-
reason: 'closed-not-planned',
|
|
85
|
-
};
|
|
145
|
+
const result = noopResult(storyId, 'closed-not-planned');
|
|
86
146
|
const terminal = buildTerminalEnvelope({
|
|
87
147
|
storyId,
|
|
88
148
|
status: 'failed',
|
|
@@ -95,17 +155,12 @@ async function alreadyClosedResult(storyId, stateReason = null, config) {
|
|
|
95
155
|
nextCommand: null,
|
|
96
156
|
elapsedSeconds: 0,
|
|
97
157
|
});
|
|
98
|
-
await emitTerminal({ terminal, result, config });
|
|
158
|
+
await emitTerminal({ terminal, result, config, workerTokens });
|
|
99
159
|
return { success: false, result, terminal };
|
|
100
160
|
}
|
|
101
161
|
|
|
102
162
|
progress('NOOP', `Story #${storyId} is already closed. Nothing to do.`);
|
|
103
|
-
const result =
|
|
104
|
-
storyId,
|
|
105
|
-
standalone: true,
|
|
106
|
-
action: 'noop',
|
|
107
|
-
reason: 'already-closed',
|
|
108
|
-
};
|
|
163
|
+
const result = noopResult(storyId, 'already-closed');
|
|
109
164
|
const terminal = buildTerminalEnvelope({
|
|
110
165
|
storyId,
|
|
111
166
|
status: 'landed',
|
|
@@ -113,7 +168,7 @@ async function alreadyClosedResult(storyId, stateReason = null, config) {
|
|
|
113
168
|
nextCommand: null,
|
|
114
169
|
elapsedSeconds: 0,
|
|
115
170
|
});
|
|
116
|
-
await emitTerminal({ terminal, result, config });
|
|
171
|
+
await emitTerminal({ terminal, result, config, workerTokens });
|
|
117
172
|
return { success: true, result, terminal };
|
|
118
173
|
}
|
|
119
174
|
|
|
@@ -136,13 +191,9 @@ async function preflightBlockedResult({
|
|
|
136
191
|
preflight,
|
|
137
192
|
config,
|
|
138
193
|
startedAtMs,
|
|
194
|
+
workerTokens = null,
|
|
139
195
|
}) {
|
|
140
|
-
const result = {
|
|
141
|
-
storyId,
|
|
142
|
-
standalone: true,
|
|
143
|
-
action: 'noop',
|
|
144
|
-
reason: `graphql-preflight-${preflight.verdict}`,
|
|
145
|
-
};
|
|
196
|
+
const result = noopResult(storyId, `graphql-preflight-${preflight.verdict}`);
|
|
146
197
|
const terminal = buildTerminalEnvelope({
|
|
147
198
|
storyId,
|
|
148
199
|
status: 'blocked',
|
|
@@ -155,7 +206,7 @@ async function preflightBlockedResult({
|
|
|
155
206
|
nextCommand: NEXT_COMMANDS.close(storyId),
|
|
156
207
|
elapsedSeconds: elapsedSecondsSince(startedAtMs),
|
|
157
208
|
});
|
|
158
|
-
await emitTerminal({ terminal, result, config });
|
|
209
|
+
await emitTerminal({ terminal, result, config, workerTokens });
|
|
159
210
|
return { success: false, result, terminal };
|
|
160
211
|
}
|
|
161
212
|
|
|
@@ -201,6 +252,8 @@ function resolveWorktreePath({ cwd, config, storyId }) {
|
|
|
201
252
|
* the validated tree is the pushed tree (base-sync's merge commit included),
|
|
202
253
|
* and a cheap conflict is found before the expensive gates.
|
|
203
254
|
*
|
|
255
|
+
* @param {object} ctx
|
|
256
|
+
* @param {object} deps
|
|
204
257
|
* @returns {Promise<{
|
|
205
258
|
* validationGates: Record<string, string>|null,
|
|
206
259
|
* lockWait: { waitedSeconds: number, expired: boolean }|null,
|
|
@@ -208,49 +261,37 @@ function resolveWorktreePath({ cwd, config, storyId }) {
|
|
|
208
261
|
* }>} `validationGates` is null when skipped; `pending` means a full-suite
|
|
209
262
|
* lock wait expired and the gates deferred.
|
|
210
263
|
*/
|
|
211
|
-
async function runPrePushPhases({
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
baseBranch,
|
|
216
|
-
baseConfirmed,
|
|
217
|
-
storyBranch,
|
|
218
|
-
storyId,
|
|
219
|
-
provider,
|
|
220
|
-
skipValidation,
|
|
221
|
-
skipSync,
|
|
222
|
-
injectedSync,
|
|
223
|
-
injectedGitSpawn,
|
|
224
|
-
setPhase = () => {},
|
|
225
|
-
setObservedGates = () => {},
|
|
226
|
-
}) {
|
|
264
|
+
async function runPrePushPhases(ctx, deps) {
|
|
265
|
+
const { options, worktreePath, baseBranch, storyBranch, storyId, setPhase } =
|
|
266
|
+
ctx;
|
|
267
|
+
const { cwd } = options;
|
|
227
268
|
setPhase('wrong-tree-guard');
|
|
228
269
|
await runWrongTreeGuardPhase({
|
|
229
270
|
cwd,
|
|
230
271
|
worktreePath,
|
|
231
272
|
baseBranch,
|
|
232
273
|
storyId,
|
|
233
|
-
provider,
|
|
274
|
+
provider: deps.provider,
|
|
234
275
|
progress,
|
|
235
|
-
gitSpawn:
|
|
276
|
+
gitSpawn: deps.gitSpawn,
|
|
236
277
|
});
|
|
237
|
-
if (!skipSync) {
|
|
278
|
+
if (!options.skipSync) {
|
|
238
279
|
setPhase('base-sync');
|
|
239
280
|
await runBaseSyncPhase({
|
|
240
281
|
cwd,
|
|
241
282
|
worktreePath,
|
|
242
283
|
baseBranch,
|
|
243
|
-
baseConfirmed,
|
|
284
|
+
baseConfirmed: ctx.baseConfirmed,
|
|
244
285
|
storyBranch,
|
|
245
286
|
storyId,
|
|
246
|
-
provider,
|
|
247
|
-
injectedSync,
|
|
287
|
+
provider: deps.provider,
|
|
288
|
+
injectedSync: deps.sync,
|
|
248
289
|
progress,
|
|
249
290
|
});
|
|
250
291
|
} else {
|
|
251
292
|
progress('SYNC', '⏭ Skipped (--skip-sync).');
|
|
252
293
|
}
|
|
253
|
-
if (skipValidation) {
|
|
294
|
+
if (options.skipValidation) {
|
|
254
295
|
progress('VALIDATE', '⏭ Skipped (--skip-validation).');
|
|
255
296
|
return { validationGates: null, lockWait: null, pending: false };
|
|
256
297
|
}
|
|
@@ -260,7 +301,7 @@ async function runPrePushPhases({
|
|
|
260
301
|
validation = await runCloseValidationPhase({
|
|
261
302
|
cwd,
|
|
262
303
|
worktreePath,
|
|
263
|
-
config,
|
|
304
|
+
config: deps.config,
|
|
264
305
|
baseBranch,
|
|
265
306
|
storyBranch,
|
|
266
307
|
storyId,
|
|
@@ -271,12 +312,12 @@ async function runPrePushPhases({
|
|
|
271
312
|
} catch (err) {
|
|
272
313
|
// The gate that died is all this run observed; the envelope claims no more.
|
|
273
314
|
if (typeof err?.closeGate === 'string') {
|
|
274
|
-
setObservedGates({ [err.closeGate]: 'failed' });
|
|
315
|
+
ctx.setObservedGates({ [err.closeGate]: 'failed' });
|
|
275
316
|
}
|
|
276
317
|
throw err;
|
|
277
318
|
}
|
|
278
319
|
const gates = validation?.gates ?? null;
|
|
279
|
-
setObservedGates(gates);
|
|
320
|
+
ctx.setObservedGates(gates);
|
|
280
321
|
return {
|
|
281
322
|
validationGates: gates,
|
|
282
323
|
lockWait: validation?.lockWait ?? null,
|
|
@@ -284,25 +325,64 @@ async function runPrePushPhases({
|
|
|
284
325
|
};
|
|
285
326
|
}
|
|
286
327
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
328
|
+
/**
|
|
329
|
+
* An overridden Story proceeds; anything else emits friction, blocks, throws.
|
|
330
|
+
*
|
|
331
|
+
* @param {object} ctx
|
|
332
|
+
* @param {object} deps
|
|
333
|
+
* @param {{ prUrl: string, prNumber: number|null, reviewOutcome: object }} pr
|
|
334
|
+
* @returns {Promise<object>}
|
|
335
|
+
*/
|
|
336
|
+
async function resolveReviewHalt(
|
|
337
|
+
ctx,
|
|
338
|
+
deps,
|
|
339
|
+
{ prUrl, prNumber, reviewOutcome },
|
|
340
|
+
) {
|
|
341
|
+
const { storyId } = ctx;
|
|
342
|
+
const criticalCount = reviewOutcome.severity?.critical ?? 0;
|
|
343
|
+
// Checked before the blocked transition: an overridden Story proceeds.
|
|
344
|
+
if (ctx.options.overrideReviewBlock) {
|
|
345
|
+
return await handleOverriddenReviewBlock({
|
|
346
|
+
provider: deps.provider,
|
|
347
|
+
storyId,
|
|
348
|
+
prUrl,
|
|
349
|
+
prNumber,
|
|
350
|
+
criticalCount,
|
|
351
|
+
criticalByProvider: reviewOutcome.criticalByProvider,
|
|
352
|
+
reason: ctx.options.overrideReviewBlock,
|
|
353
|
+
config: deps.config,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
await emitReviewBlockedFriction({
|
|
357
|
+
storyId,
|
|
358
|
+
prNumber,
|
|
359
|
+
criticalCount,
|
|
360
|
+
criticalByProvider: reviewOutcome.criticalByProvider,
|
|
361
|
+
config: deps.config,
|
|
362
|
+
});
|
|
363
|
+
await handleCriticalReviewBlock({
|
|
364
|
+
provider: deps.provider,
|
|
365
|
+
storyId,
|
|
366
|
+
prUrl,
|
|
367
|
+
criticalCount,
|
|
368
|
+
});
|
|
369
|
+
throw new Error(
|
|
370
|
+
`[single-story-close] Story-scope review reported ${criticalCount} critical blocker(s) on PR ${prUrl}. ` +
|
|
371
|
+
'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/mandrel-deliver`. ' +
|
|
372
|
+
'If you have reviewed a finding and judged it wrong, re-run with ' +
|
|
373
|
+
'`--override-review-block "<reason>"` rather than merging by hand.',
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function openAndReviewPr(ctx, deps) {
|
|
378
|
+
const { options, worktreePath, story, storyId, storyBranch, baseBranch } =
|
|
379
|
+
ctx;
|
|
380
|
+
const { cwd } = options;
|
|
381
|
+
ctx.setPhase('push');
|
|
302
382
|
// Push from the worktree so `pre-push` measures the tree being sent; the
|
|
303
383
|
// ref-based reads below resolve identically from the shared `.git`.
|
|
304
384
|
pushStoryBranch({ cwd, worktreePath, storyBranch, gitSync, progress });
|
|
305
|
-
setPhase('pull-request');
|
|
385
|
+
ctx.setPhase('pull-request');
|
|
306
386
|
const { url: prUrl, alreadyMerged } = await ensurePullRequestWith({
|
|
307
387
|
cwd,
|
|
308
388
|
storyId,
|
|
@@ -310,7 +390,7 @@ async function openAndReviewPr({
|
|
|
310
390
|
storyBody: story.body,
|
|
311
391
|
storyBranch,
|
|
312
392
|
baseBranch,
|
|
313
|
-
gh:
|
|
393
|
+
gh: deps.gh,
|
|
314
394
|
progress,
|
|
315
395
|
});
|
|
316
396
|
const prNumber = parsePrNumber(prUrl);
|
|
@@ -318,7 +398,7 @@ async function openAndReviewPr({
|
|
|
318
398
|
if (alreadyMerged) {
|
|
319
399
|
return { prUrl, prNumber, alreadyMerged: true };
|
|
320
400
|
}
|
|
321
|
-
setPhase('code-review');
|
|
401
|
+
ctx.setPhase('code-review');
|
|
322
402
|
const reviewOutcome = await runStoryScopeReview({
|
|
323
403
|
cwd,
|
|
324
404
|
storyId,
|
|
@@ -326,56 +406,24 @@ async function openAndReviewPr({
|
|
|
326
406
|
baseBranch,
|
|
327
407
|
prUrl,
|
|
328
408
|
prNumber,
|
|
329
|
-
provider,
|
|
330
|
-
runCodeReviewFn:
|
|
409
|
+
provider: deps.provider,
|
|
410
|
+
runCodeReviewFn: deps.runCodeReview,
|
|
331
411
|
gitSpawnFn: gitSpawn,
|
|
332
412
|
progress,
|
|
333
413
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const override = await handleOverriddenReviewBlock({
|
|
339
|
-
provider,
|
|
340
|
-
storyId,
|
|
341
|
-
prUrl,
|
|
342
|
-
prNumber,
|
|
343
|
-
criticalCount,
|
|
344
|
-
reason: overrideReviewBlock,
|
|
345
|
-
config,
|
|
346
|
-
});
|
|
347
|
-
return {
|
|
348
|
-
prUrl,
|
|
349
|
-
prNumber,
|
|
350
|
-
alreadyMerged: false,
|
|
351
|
-
reviewOverride: override,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
await handleCriticalReviewBlock({
|
|
355
|
-
provider,
|
|
356
|
-
storyId,
|
|
357
|
-
prUrl,
|
|
358
|
-
criticalCount,
|
|
359
|
-
});
|
|
360
|
-
throw new Error(
|
|
361
|
-
`[single-story-close] Story-scope review reported ${criticalCount} critical blocker(s) on PR ${prUrl}. ` +
|
|
362
|
-
'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/mandrel-deliver`. ' +
|
|
363
|
-
'If you have reviewed a finding and judged it wrong, re-run with ' +
|
|
364
|
-
'`--override-review-block "<reason>"` rather than merging by hand.',
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
return { prUrl, prNumber, alreadyMerged: false, reviewOverride: null };
|
|
414
|
+
const reviewOverride = reviewOutcome.halted
|
|
415
|
+
? await resolveReviewHalt(ctx, deps, { prUrl, prNumber, reviewOutcome })
|
|
416
|
+
: null;
|
|
417
|
+
return { prUrl, prNumber, alreadyMerged: false, reviewOverride };
|
|
368
418
|
}
|
|
369
419
|
|
|
370
|
-
async function releaseLease({
|
|
371
|
-
provider,
|
|
372
|
-
storyId,
|
|
373
|
-
config,
|
|
374
|
-
injectedReleaseLease,
|
|
375
|
-
}) {
|
|
420
|
+
async function releaseLease({ storyId }, deps) {
|
|
376
421
|
try {
|
|
377
|
-
const
|
|
378
|
-
|
|
422
|
+
const outcome = await deps.releaseLease({
|
|
423
|
+
provider: deps.provider,
|
|
424
|
+
storyId,
|
|
425
|
+
config: deps.config,
|
|
426
|
+
});
|
|
379
427
|
progress(
|
|
380
428
|
'LEASE',
|
|
381
429
|
outcome.released
|
|
@@ -399,14 +447,13 @@ async function releaseLease({
|
|
|
399
447
|
*
|
|
400
448
|
* @template T
|
|
401
449
|
* @param {() => Promise<T>} run
|
|
402
|
-
* @param {{ provider: object, storyId: number, config: object, injectedReleaseLease?: Function }} leaseArgs
|
|
403
450
|
* @returns {Promise<T>}
|
|
404
451
|
*/
|
|
405
|
-
async function releaseLeaseOnBlock(run,
|
|
452
|
+
async function releaseLeaseOnBlock(run, ctx, deps) {
|
|
406
453
|
try {
|
|
407
454
|
return await run();
|
|
408
455
|
} catch (err) {
|
|
409
|
-
await releaseLease(
|
|
456
|
+
await releaseLease(ctx, deps);
|
|
410
457
|
throw err;
|
|
411
458
|
}
|
|
412
459
|
}
|
|
@@ -470,6 +517,29 @@ function closeResult({
|
|
|
470
517
|
};
|
|
471
518
|
}
|
|
472
519
|
|
|
520
|
+
/**
|
|
521
|
+
* An injected double wins, else the real one; an absent optional seam stays
|
|
522
|
+
* `undefined` so its phase applies its own default.
|
|
523
|
+
*
|
|
524
|
+
* @param {{ cwd: string }} options
|
|
525
|
+
* @param {object} injected
|
|
526
|
+
* @returns {object}
|
|
527
|
+
*/
|
|
528
|
+
function resolveCloseDeps(options, injected) {
|
|
529
|
+
const config = injected.injectedConfig || resolveConfig({ cwd: options.cwd });
|
|
530
|
+
return {
|
|
531
|
+
config,
|
|
532
|
+
provider: injected.injectedProvider || createProvider(config),
|
|
533
|
+
notify: injected.injectedNotify,
|
|
534
|
+
sync: injected.injectedSync,
|
|
535
|
+
runCodeReview: injected.injectedRunCodeReview ?? runCodeReviewDefault,
|
|
536
|
+
gh: injected.injectedGh,
|
|
537
|
+
gitSpawn: injected.injectedGitSpawn,
|
|
538
|
+
releaseLease: injected.injectedReleaseLease ?? releaseStoryLease,
|
|
539
|
+
graphqlProbe: injected.injectedGraphqlProbe,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
473
543
|
export async function runSingleStoryClose({
|
|
474
544
|
storyId: storyIdParam,
|
|
475
545
|
cwd: cwdParam,
|
|
@@ -482,15 +552,8 @@ export async function runSingleStoryClose({
|
|
|
482
552
|
mergeWatchMode: mergeWatchModeParam,
|
|
483
553
|
rerunAdvisory: rerunAdvisoryParam,
|
|
484
554
|
overrideReviewBlock: overrideReviewBlockParam,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
injectedNotify,
|
|
488
|
-
injectedSync,
|
|
489
|
-
injectedRunCodeReview,
|
|
490
|
-
injectedGh,
|
|
491
|
-
injectedGitSpawn,
|
|
492
|
-
injectedReleaseLease,
|
|
493
|
-
injectedGraphqlProbe,
|
|
555
|
+
workerTokens: workerTokensParam,
|
|
556
|
+
...injected
|
|
494
557
|
} = {}) {
|
|
495
558
|
const options = parseCloseOptions({
|
|
496
559
|
storyIdParam,
|
|
@@ -504,10 +567,11 @@ export async function runSingleStoryClose({
|
|
|
504
567
|
mergeWatchModeParam,
|
|
505
568
|
rerunAdvisoryParam,
|
|
506
569
|
overrideReviewBlockParam,
|
|
570
|
+
workerTokensParam,
|
|
507
571
|
});
|
|
508
572
|
if (!options.storyId) {
|
|
509
573
|
throw new Error(
|
|
510
|
-
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>] [--merge-watch-mode <sync|async>] [--rerun-advisory <n>] [--override-review-block <reason>]',
|
|
574
|
+
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>] [--merge-watch-mode <sync|async>] [--rerun-advisory <n>] [--override-review-block <reason>] [--worker-tokens <n>]',
|
|
511
575
|
);
|
|
512
576
|
}
|
|
513
577
|
|
|
@@ -516,29 +580,23 @@ export async function runSingleStoryClose({
|
|
|
516
580
|
// builds the `failed` envelope from those tags.
|
|
517
581
|
let phase = 'init';
|
|
518
582
|
let observedGates = null;
|
|
583
|
+
const phaseTimer = createPhaseTimer();
|
|
519
584
|
const setPhase = (next) => {
|
|
520
585
|
phase = next;
|
|
586
|
+
phaseTimer.enter(next);
|
|
521
587
|
};
|
|
522
588
|
const setObservedGates = (gates) => {
|
|
523
589
|
observedGates = gates;
|
|
524
590
|
};
|
|
525
591
|
try {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
setPhase,
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
injectedConfig,
|
|
532
|
-
injectedNotify,
|
|
533
|
-
injectedSync,
|
|
534
|
-
injectedRunCodeReview,
|
|
535
|
-
injectedGh,
|
|
536
|
-
injectedGitSpawn,
|
|
537
|
-
injectedReleaseLease,
|
|
538
|
-
injectedGraphqlProbe,
|
|
539
|
-
});
|
|
592
|
+
const startedAtMs = Date.now();
|
|
593
|
+
return await runClosePipeline(
|
|
594
|
+
{ options, setPhase, setObservedGates, phaseTimer, startedAtMs },
|
|
595
|
+
resolveCloseDeps(options, injected),
|
|
596
|
+
);
|
|
540
597
|
} catch (err) {
|
|
541
598
|
if (err && typeof err === 'object') {
|
|
599
|
+
err.closePhaseDurations = phaseTimer.finish();
|
|
542
600
|
if (!err.closePhase) err.closePhase = phase;
|
|
543
601
|
if (!err.closeGates && observedGates) err.closeGates = observedGates;
|
|
544
602
|
}
|
|
@@ -617,6 +675,10 @@ async function finishWithMergeWait(prCtx, deps) {
|
|
|
617
675
|
progress,
|
|
618
676
|
injectedGh: deps.injectedGh,
|
|
619
677
|
injectedNotify: deps.injectedNotify,
|
|
678
|
+
runPostLandTailFn: (args) => {
|
|
679
|
+
deps.setPhase('post-land');
|
|
680
|
+
return runPostLandTail(args);
|
|
681
|
+
},
|
|
620
682
|
});
|
|
621
683
|
const terminal = terminalFromWaitOutcome({
|
|
622
684
|
waitOutcome,
|
|
@@ -650,7 +712,13 @@ async function finishWithMergeWait(prCtx, deps) {
|
|
|
650
712
|
}),
|
|
651
713
|
landCompleted: waitOutcome.confirmed === true,
|
|
652
714
|
});
|
|
653
|
-
await emitTerminal({
|
|
715
|
+
await emitTerminal({
|
|
716
|
+
terminal,
|
|
717
|
+
result,
|
|
718
|
+
config: prCtx.config,
|
|
719
|
+
phaseTimer: prCtx.phaseTimer,
|
|
720
|
+
workerTokens: prCtx.workerTokens,
|
|
721
|
+
});
|
|
654
722
|
reportWaitTerminal(terminal, { storyId: prCtx.storyId, prUrl: prCtx.prUrl });
|
|
655
723
|
return { success: terminal.status === 'landed', result, terminal };
|
|
656
724
|
}
|
|
@@ -698,7 +766,13 @@ async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
|
698
766
|
nextCommand: NEXT_COMMANDS.confirmMerge(prCtx.storyId),
|
|
699
767
|
elapsedSeconds: elapsedSecondsSince(prCtx.startedAtMs),
|
|
700
768
|
});
|
|
701
|
-
await emitTerminal({
|
|
769
|
+
await emitTerminal({
|
|
770
|
+
terminal,
|
|
771
|
+
result,
|
|
772
|
+
config: prCtx.config,
|
|
773
|
+
phaseTimer: prCtx.phaseTimer,
|
|
774
|
+
workerTokens: prCtx.workerTokens,
|
|
775
|
+
});
|
|
702
776
|
progress(
|
|
703
777
|
'DONE',
|
|
704
778
|
`✅ Story #${prCtx.storyId}: PR ready → ${prCtx.prUrl} (${waitForMergeReason})`,
|
|
@@ -714,13 +788,16 @@ async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
|
714
788
|
* config: object, startedAtMs: number }} ctx
|
|
715
789
|
* @returns {Promise<{ success: false, result: object, terminal: object }>}
|
|
716
790
|
*/
|
|
717
|
-
async function finishDeferred(
|
|
791
|
+
async function finishDeferred(
|
|
792
|
+
lockWait,
|
|
793
|
+
{ config, startedAtMs, phaseTimer, workerTokens, ...ids },
|
|
794
|
+
) {
|
|
718
795
|
const { result, terminal, note } = lockWaitPending({
|
|
719
796
|
...ids,
|
|
720
797
|
lockWait,
|
|
721
798
|
elapsedSeconds: elapsedSecondsSince(startedAtMs),
|
|
722
799
|
});
|
|
723
|
-
await emitTerminal({ terminal, result, config });
|
|
800
|
+
await emitTerminal({ terminal, result, config, phaseTimer, workerTokens });
|
|
724
801
|
progress('PENDING', note);
|
|
725
802
|
return { success: false, result, terminal };
|
|
726
803
|
}
|
|
@@ -755,209 +832,212 @@ function reportOperatorMergeSkip({
|
|
|
755
832
|
);
|
|
756
833
|
}
|
|
757
834
|
|
|
758
|
-
async function
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
injectedReleaseLease,
|
|
770
|
-
injectedGraphqlProbe,
|
|
771
|
-
}) {
|
|
772
|
-
const startedAtMs = Date.now();
|
|
773
|
-
const config = injectedConfig || resolveConfig({ cwd: options.cwd });
|
|
774
|
-
const provider = injectedProvider || createProvider(config);
|
|
775
|
-
const storyBranch = getStoryBranch(options.storyId);
|
|
776
|
-
|
|
777
|
-
progress('INIT', `Closing standalone Story #${options.storyId}...`);
|
|
778
|
-
const story = await provider.getTicket(options.storyId);
|
|
779
|
-
if (story.state === 'closed') {
|
|
780
|
-
return await alreadyClosedResult(
|
|
781
|
-
options.storyId,
|
|
782
|
-
story.stateReason,
|
|
783
|
-
config,
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const leaseArgs = {
|
|
788
|
-
provider,
|
|
789
|
-
storyId: options.storyId,
|
|
790
|
-
config,
|
|
791
|
-
injectedReleaseLease,
|
|
792
|
-
};
|
|
835
|
+
async function loadStoryPhase(ctx, deps) {
|
|
836
|
+
progress('INIT', `Closing standalone Story #${ctx.storyId}...`);
|
|
837
|
+
ctx.story = await deps.provider.getTicket(ctx.storyId);
|
|
838
|
+
if (ctx.story.state !== 'closed') return null;
|
|
839
|
+
return await alreadyClosedResult(
|
|
840
|
+
ctx.storyId,
|
|
841
|
+
ctx.story.stateReason,
|
|
842
|
+
deps.config,
|
|
843
|
+
ctx.workerTokens,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
793
846
|
|
|
794
|
-
|
|
847
|
+
/** One cheap GraphQL read here beats the gate chain plus a push. */
|
|
848
|
+
async function graphqlPreflightPhase(ctx, deps) {
|
|
795
849
|
const preflight = await runGraphqlPreflight({
|
|
796
|
-
storyId:
|
|
797
|
-
provider,
|
|
850
|
+
storyId: ctx.storyId,
|
|
851
|
+
provider: deps.provider,
|
|
798
852
|
progress,
|
|
799
|
-
ghFacade:
|
|
800
|
-
probe:
|
|
853
|
+
ghFacade: deps.gh,
|
|
854
|
+
probe: deps.graphqlProbe,
|
|
801
855
|
});
|
|
802
|
-
if (preflight)
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
856
|
+
if (!preflight) return null;
|
|
857
|
+
await releaseLease(ctx, deps);
|
|
858
|
+
return await preflightBlockedResult({
|
|
859
|
+
storyId: ctx.storyId,
|
|
860
|
+
preflight,
|
|
861
|
+
config: deps.config,
|
|
862
|
+
startedAtMs: ctx.startedAtMs,
|
|
863
|
+
workerTokens: ctx.workerTokens,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
811
866
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
867
|
+
/**
|
|
868
|
+
* The base the run was SEEDED from (init receipt); throws before any merge
|
|
869
|
+
* when it disagrees with current config.
|
|
870
|
+
*/
|
|
871
|
+
async function resolveBasePhase(ctx, deps) {
|
|
872
|
+
const { values, confirmed } = await resolveRunScopedConfig({
|
|
873
|
+
storyId: ctx.storyId,
|
|
874
|
+
config: deps.config,
|
|
875
|
+
progress,
|
|
876
|
+
});
|
|
877
|
+
ctx.baseBranch = values.baseBranch;
|
|
878
|
+
ctx.baseConfirmed = confirmed;
|
|
879
|
+
ctx.worktreePath = resolveWorktreePath({
|
|
880
|
+
cwd: ctx.options.cwd,
|
|
881
|
+
config: deps.config,
|
|
882
|
+
storyId: ctx.storyId,
|
|
883
|
+
});
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
821
886
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
887
|
+
async function prePushPhase(ctx, deps) {
|
|
888
|
+
ctx.prePush = await releaseLeaseOnBlock(
|
|
889
|
+
() => runPrePushPhases(ctx, deps),
|
|
890
|
+
ctx,
|
|
891
|
+
deps,
|
|
892
|
+
);
|
|
893
|
+
if (!ctx.prePush.pending) return null;
|
|
894
|
+
return await finishDeferred(ctx.prePush.lockWait, {
|
|
895
|
+
storyId: ctx.storyId,
|
|
896
|
+
storyBranch: ctx.storyBranch,
|
|
897
|
+
baseBranch: ctx.baseBranch,
|
|
898
|
+
config: deps.config,
|
|
899
|
+
startedAtMs: ctx.startedAtMs,
|
|
900
|
+
phaseTimer: ctx.phaseTimer,
|
|
901
|
+
workerTokens: ctx.workerTokens,
|
|
826
902
|
});
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
storyBranch,
|
|
835
|
-
provider,
|
|
836
|
-
worktreePath,
|
|
837
|
-
injectedSync,
|
|
838
|
-
injectedGitSpawn,
|
|
839
|
-
setPhase,
|
|
840
|
-
setObservedGates,
|
|
841
|
-
}),
|
|
842
|
-
leaseArgs,
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function openPrPhase(ctx, deps) {
|
|
906
|
+
ctx.pr = await releaseLeaseOnBlock(
|
|
907
|
+
() => openAndReviewPr(ctx, deps),
|
|
908
|
+
ctx,
|
|
909
|
+
deps,
|
|
843
910
|
);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
storyId: options.storyId,
|
|
847
|
-
storyBranch,
|
|
848
|
-
baseBranch,
|
|
849
|
-
config,
|
|
850
|
-
startedAtMs,
|
|
851
|
-
});
|
|
852
|
-
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
853
913
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
config,
|
|
866
|
-
overrideReviewBlock: options.overrideReviewBlock,
|
|
867
|
-
injectedGh,
|
|
868
|
-
injectedRunCodeReview,
|
|
869
|
-
setPhase,
|
|
870
|
-
}),
|
|
871
|
-
leaseArgs,
|
|
872
|
-
);
|
|
873
|
-
// Reap BEFORE the arm: `gh pr merge --delete-branch` may merge at once and
|
|
874
|
-
// then fail deleting a branch a live worktree holds, reading as a failed
|
|
875
|
-
// arm. Safe — the work is pushed, and a dirty tree is still refused.
|
|
876
|
-
const worktreeReaped = await reapWorktreePhase({
|
|
877
|
-
cwd: options.cwd,
|
|
878
|
-
storyId: options.storyId,
|
|
879
|
-
worktreePath,
|
|
880
|
-
wtIsolation: config.delivery?.worktreeIsolation,
|
|
914
|
+
/**
|
|
915
|
+
* Reap BEFORE the arm: `gh pr merge --delete-branch` may merge at once and
|
|
916
|
+
* then fail deleting a branch a live worktree holds, reading as a failed arm.
|
|
917
|
+
* Safe — the work is pushed, and a dirty tree is still refused.
|
|
918
|
+
*/
|
|
919
|
+
async function reapWorktreePhaseStep(ctx, deps) {
|
|
920
|
+
ctx.worktreeReaped = await reapWorktreePhase({
|
|
921
|
+
cwd: ctx.options.cwd,
|
|
922
|
+
storyId: ctx.storyId,
|
|
923
|
+
worktreePath: ctx.worktreePath,
|
|
924
|
+
wtIsolation: deps.config.delivery?.worktreeIsolation,
|
|
881
925
|
progress,
|
|
882
926
|
WorktreeManager,
|
|
883
927
|
});
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** No lease release here: only the post-land tail releases it. */
|
|
932
|
+
async function armPhase(ctx, deps) {
|
|
933
|
+
ctx.setPhase('auto-merge');
|
|
934
|
+
const ciDelivery = getCiDelivery(deps.config);
|
|
935
|
+
const { prUrl, prNumber, alreadyMerged } = ctx.pr;
|
|
936
|
+
ctx.arm = await resolveAutoMergeOutcome({
|
|
893
937
|
alreadyMerged,
|
|
894
|
-
cwd: options.cwd,
|
|
938
|
+
cwd: ctx.options.cwd,
|
|
895
939
|
prNumber,
|
|
896
940
|
prUrl,
|
|
897
|
-
noAutoMerge: options.noAutoMerge,
|
|
941
|
+
noAutoMerge: ctx.options.noAutoMerge,
|
|
898
942
|
autoMergePolicy: ciDelivery.autoMerge,
|
|
899
943
|
blockOnAdvisoryFailure: ciDelivery.blockOnAdvisoryFailure,
|
|
900
944
|
advisoryAllowlist: ciDelivery.advisoryAllowlist,
|
|
901
|
-
gh:
|
|
945
|
+
gh: deps.gh,
|
|
902
946
|
progress,
|
|
903
947
|
});
|
|
904
948
|
await flipLabelAndNotify({
|
|
905
|
-
provider,
|
|
906
|
-
notifyFn:
|
|
907
|
-
storyId:
|
|
908
|
-
story,
|
|
949
|
+
provider: deps.provider,
|
|
950
|
+
notifyFn: deps.notify,
|
|
951
|
+
storyId: ctx.storyId,
|
|
952
|
+
story: ctx.story,
|
|
909
953
|
prUrl,
|
|
910
|
-
autoMergeEnabled,
|
|
911
|
-
autoMergeReason,
|
|
912
|
-
config,
|
|
954
|
+
autoMergeEnabled: ctx.arm.autoMergeEnabled,
|
|
955
|
+
autoMergeReason: ctx.arm.autoMergeReason,
|
|
956
|
+
config: deps.config,
|
|
913
957
|
progress,
|
|
914
958
|
});
|
|
915
|
-
|
|
916
|
-
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
917
961
|
|
|
918
|
-
|
|
919
|
-
|
|
962
|
+
/** Wait-for-merge needs the arm outcome, so it is resolved here. */
|
|
963
|
+
async function finishPhase(ctx, deps) {
|
|
964
|
+
const { options, arm } = ctx;
|
|
920
965
|
const { waitForMerge, reason: waitForMergeReason } = resolveWaitForMerge({
|
|
921
966
|
waitForMergeExplicit: options.waitForMergeExplicit,
|
|
922
967
|
noWaitForMerge: options.noWaitForMerge,
|
|
923
|
-
config,
|
|
924
|
-
autoMergeReason,
|
|
968
|
+
config: deps.config,
|
|
969
|
+
autoMergeReason: arm.autoMergeReason,
|
|
925
970
|
});
|
|
926
971
|
reportOperatorMergeSkip({
|
|
927
972
|
waitForMergeReason,
|
|
928
|
-
autoMergeReason,
|
|
929
|
-
storyId:
|
|
973
|
+
autoMergeReason: arm.autoMergeReason,
|
|
974
|
+
storyId: ctx.storyId,
|
|
930
975
|
waitForMergeExplicit: options.waitForMergeExplicit,
|
|
931
976
|
});
|
|
932
977
|
const prCtx = {
|
|
933
|
-
storyId:
|
|
934
|
-
storyBranch,
|
|
935
|
-
baseBranch,
|
|
936
|
-
prNumber,
|
|
937
|
-
prUrl,
|
|
938
|
-
autoMergeEnabled,
|
|
939
|
-
autoMergeReason,
|
|
940
|
-
advisoryGate,
|
|
941
|
-
worktreeReaped,
|
|
942
|
-
localCleanupDeferred,
|
|
943
|
-
directMerged,
|
|
944
|
-
config,
|
|
945
|
-
startedAtMs,
|
|
946
|
-
|
|
947
|
-
|
|
978
|
+
storyId: ctx.storyId,
|
|
979
|
+
storyBranch: ctx.storyBranch,
|
|
980
|
+
baseBranch: ctx.baseBranch,
|
|
981
|
+
prNumber: ctx.pr.prNumber,
|
|
982
|
+
prUrl: ctx.pr.prUrl,
|
|
983
|
+
autoMergeEnabled: arm.autoMergeEnabled,
|
|
984
|
+
autoMergeReason: arm.autoMergeReason,
|
|
985
|
+
advisoryGate: arm.advisoryGate,
|
|
986
|
+
worktreeReaped: ctx.worktreeReaped,
|
|
987
|
+
localCleanupDeferred: arm.localCleanupDeferred,
|
|
988
|
+
directMerged: arm.directMerged,
|
|
989
|
+
config: deps.config,
|
|
990
|
+
startedAtMs: ctx.startedAtMs,
|
|
991
|
+
phaseTimer: ctx.phaseTimer,
|
|
992
|
+
workerTokens: ctx.workerTokens,
|
|
993
|
+
lockWait: ctx.prePush.lockWait,
|
|
994
|
+
gates: closeEnvelopeGates(
|
|
995
|
+
options,
|
|
996
|
+
ctx.prePush.validationGates,
|
|
997
|
+
ctx.pr.reviewOverride,
|
|
998
|
+
),
|
|
948
999
|
};
|
|
1000
|
+
if (!waitForMerge) {
|
|
1001
|
+
return await finishWithoutMergeWait(prCtx, waitForMergeReason);
|
|
1002
|
+
}
|
|
1003
|
+
return await finishWithMergeWait(prCtx, {
|
|
1004
|
+
cwd: options.cwd,
|
|
1005
|
+
provider: deps.provider,
|
|
1006
|
+
maxWaitSeconds: options.maxWaitSeconds,
|
|
1007
|
+
mergeWatchMode: options.mergeWatchMode,
|
|
1008
|
+
rerunAdvisory: options.rerunAdvisory,
|
|
1009
|
+
setPhase: ctx.setPhase,
|
|
1010
|
+
injectedGh: deps.gh,
|
|
1011
|
+
injectedNotify: deps.notify,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
949
1014
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1015
|
+
/** Each phase extends the shared context; a non-null return ends the run. */
|
|
1016
|
+
const CLOSE_PIPELINE = Object.freeze([
|
|
1017
|
+
loadStoryPhase,
|
|
1018
|
+
graphqlPreflightPhase,
|
|
1019
|
+
resolveBasePhase,
|
|
1020
|
+
prePushPhase,
|
|
1021
|
+
openPrPhase,
|
|
1022
|
+
reapWorktreePhaseStep,
|
|
1023
|
+
armPhase,
|
|
1024
|
+
finishPhase,
|
|
1025
|
+
]);
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* @param {object} run
|
|
1029
|
+
* @param {object} deps
|
|
1030
|
+
* @returns {Promise<{ success: boolean, result: object, terminal: object }>}
|
|
1031
|
+
*/
|
|
1032
|
+
async function runClosePipeline(run, deps) {
|
|
1033
|
+
const ctx = {
|
|
1034
|
+
...run,
|
|
1035
|
+
storyId: run.options.storyId,
|
|
1036
|
+
storyBranch: getStoryBranch(run.options.storyId),
|
|
1037
|
+
workerTokens: resolveWorkerTokens(run.options.workerTokens),
|
|
1038
|
+
};
|
|
1039
|
+
for (const phase of CLOSE_PIPELINE) {
|
|
1040
|
+
const ending = await phase(ctx, deps);
|
|
1041
|
+
if (ending) return ending;
|
|
961
1042
|
}
|
|
962
|
-
return await finishWithoutMergeWait(prCtx, waitForMergeReason);
|
|
963
1043
|
}
|