mandrel 2.65.0 → 2.67.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 +7 -7
- package/.agents/agents/auditor.md +17 -18
- package/.agents/agents/plan-critic.md +5 -5
- package/.agents/agents/story-worker.md +5 -5
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/execution-reference.md +27 -5
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +12 -13
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +3 -2
- package/.agents/rules/git-conventions-reference.md +17 -8
- package/.agents/rules/git-conventions.md +10 -8
- package/.agents/rules/testing-standards.md +8 -7
- package/.agents/runtime-deps.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/scripts/boot-sweep.js +97 -9
- package/.agents/scripts/bootstrap.js +94 -89
- package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
- package/.agents/scripts/clean-temp.js +54 -0
- package/.agents/scripts/clean-worktrees.js +593 -0
- package/.agents/scripts/drain-pending-cleanup.js +5 -4
- package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
- package/.agents/scripts/lib/clean-temp.js +440 -0
- 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-settings-schema-delivery.js +11 -2
- 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/runtime-friction.js +2 -0
- package/.agents/scripts/lib/observability/signal-validator.js +17 -5
- package/.agents/scripts/lib/observability/source-classifier.js +3 -1
- package/.agents/scripts/lib/orchestration/code-review.js +22 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
- package/.agents/scripts/lib/signals/detectors/common.js +63 -51
- package/.agents/scripts/lib/single-story-sweep.js +2 -2
- package/.agents/scripts/lib/temp-removal.js +110 -0
- package/.agents/scripts/lib/temp-retention.js +122 -73
- package/.agents/scripts/lib/transpile.js +28 -3
- package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
- package/.agents/scripts/single-story-close.js +10 -2
- package/.agents/scripts/single-story-confirm-merge.js +267 -238
- package/.agents/scripts/single-story-init.js +120 -17
- 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/{git-cleanup.md → clean-git.md} +10 -10
- package/.agents/workflows/clean-temp.md +67 -0
- package/.agents/workflows/clean-worktrees.md +63 -0
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +11 -10
- package/.agents/workflows/helpers/audit-lens-core.md +30 -57
- package/.agents/workflows/helpers/deliver-digest.md +2 -2
- package/.agents/workflows/helpers/deliver-reference.md +3 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
- package/.agents/workflows/helpers/deliver-story.md +6 -1
- package/.agents/workflows/helpers/parallel-tooling.md +16 -18
- package/.agents/workflows/mandrel-deliver.md +1 -1
- package/.agents/workflows/mandrel-plan.md +6 -5
- package/docs/CHANGELOG.md +39 -0
- package/lib/cli/guarded-sync.js +87 -0
- package/lib/cli/sync-agents.js +9 -92
- package/lib/cli/sync-commands.js +9 -101
- package/package.json +2 -2
|
@@ -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';
|
|
@@ -80,17 +85,20 @@ function createPhaseTimer(nowMs = Date.now) {
|
|
|
80
85
|
const UNTIMED = Object.freeze({ stamp() {} });
|
|
81
86
|
|
|
82
87
|
/**
|
|
83
|
-
* The single terminal writer: the
|
|
84
|
-
* and terminal friction — so no
|
|
85
|
-
* CLI `process.exit`s as soon as
|
|
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.
|
|
86
92
|
*/
|
|
87
93
|
async function emitTerminal({
|
|
88
94
|
terminal,
|
|
89
95
|
result,
|
|
90
96
|
config,
|
|
91
97
|
phaseTimer = UNTIMED,
|
|
98
|
+
workerTokens = null,
|
|
92
99
|
}) {
|
|
93
100
|
phaseTimer.stamp(terminal);
|
|
101
|
+
await recordCloseTelemetry({ terminal, result, config, workerTokens });
|
|
94
102
|
if (result) {
|
|
95
103
|
emitTerseResult({
|
|
96
104
|
label: 'STORY CLOSE RESULT',
|
|
@@ -109,23 +117,32 @@ async function emitTerminal({
|
|
|
109
117
|
await emitTerminalFriction({ envelope: terminal, config });
|
|
110
118
|
}
|
|
111
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
|
+
|
|
112
129
|
/**
|
|
113
130
|
* Terminal for an already-closed Story. `not_planned` means nothing merged,
|
|
114
131
|
* so it fails rather than reporting `landed` (which would also unblock
|
|
115
132
|
* dependents); `completed` or null (GitHub's default) reads as landed.
|
|
116
133
|
*/
|
|
117
|
-
async function alreadyClosedResult(
|
|
134
|
+
async function alreadyClosedResult(
|
|
135
|
+
storyId,
|
|
136
|
+
stateReason = null,
|
|
137
|
+
config,
|
|
138
|
+
workerTokens = null,
|
|
139
|
+
) {
|
|
118
140
|
if (stateReason === 'not_planned') {
|
|
119
141
|
progress(
|
|
120
142
|
'NOOP',
|
|
121
143
|
`Story #${storyId} is closed as not planned — nothing to land.`,
|
|
122
144
|
);
|
|
123
|
-
const result =
|
|
124
|
-
storyId,
|
|
125
|
-
standalone: true,
|
|
126
|
-
action: 'noop',
|
|
127
|
-
reason: 'closed-not-planned',
|
|
128
|
-
};
|
|
145
|
+
const result = noopResult(storyId, 'closed-not-planned');
|
|
129
146
|
const terminal = buildTerminalEnvelope({
|
|
130
147
|
storyId,
|
|
131
148
|
status: 'failed',
|
|
@@ -138,17 +155,12 @@ async function alreadyClosedResult(storyId, stateReason = null, config) {
|
|
|
138
155
|
nextCommand: null,
|
|
139
156
|
elapsedSeconds: 0,
|
|
140
157
|
});
|
|
141
|
-
await emitTerminal({ terminal, result, config });
|
|
158
|
+
await emitTerminal({ terminal, result, config, workerTokens });
|
|
142
159
|
return { success: false, result, terminal };
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
progress('NOOP', `Story #${storyId} is already closed. Nothing to do.`);
|
|
146
|
-
const result =
|
|
147
|
-
storyId,
|
|
148
|
-
standalone: true,
|
|
149
|
-
action: 'noop',
|
|
150
|
-
reason: 'already-closed',
|
|
151
|
-
};
|
|
163
|
+
const result = noopResult(storyId, 'already-closed');
|
|
152
164
|
const terminal = buildTerminalEnvelope({
|
|
153
165
|
storyId,
|
|
154
166
|
status: 'landed',
|
|
@@ -156,7 +168,7 @@ async function alreadyClosedResult(storyId, stateReason = null, config) {
|
|
|
156
168
|
nextCommand: null,
|
|
157
169
|
elapsedSeconds: 0,
|
|
158
170
|
});
|
|
159
|
-
await emitTerminal({ terminal, result, config });
|
|
171
|
+
await emitTerminal({ terminal, result, config, workerTokens });
|
|
160
172
|
return { success: true, result, terminal };
|
|
161
173
|
}
|
|
162
174
|
|
|
@@ -179,13 +191,9 @@ async function preflightBlockedResult({
|
|
|
179
191
|
preflight,
|
|
180
192
|
config,
|
|
181
193
|
startedAtMs,
|
|
194
|
+
workerTokens = null,
|
|
182
195
|
}) {
|
|
183
|
-
const result = {
|
|
184
|
-
storyId,
|
|
185
|
-
standalone: true,
|
|
186
|
-
action: 'noop',
|
|
187
|
-
reason: `graphql-preflight-${preflight.verdict}`,
|
|
188
|
-
};
|
|
196
|
+
const result = noopResult(storyId, `graphql-preflight-${preflight.verdict}`);
|
|
189
197
|
const terminal = buildTerminalEnvelope({
|
|
190
198
|
storyId,
|
|
191
199
|
status: 'blocked',
|
|
@@ -198,7 +206,7 @@ async function preflightBlockedResult({
|
|
|
198
206
|
nextCommand: NEXT_COMMANDS.close(storyId),
|
|
199
207
|
elapsedSeconds: elapsedSecondsSince(startedAtMs),
|
|
200
208
|
});
|
|
201
|
-
await emitTerminal({ terminal, result, config });
|
|
209
|
+
await emitTerminal({ terminal, result, config, workerTokens });
|
|
202
210
|
return { success: false, result, terminal };
|
|
203
211
|
}
|
|
204
212
|
|
|
@@ -244,6 +252,8 @@ function resolveWorktreePath({ cwd, config, storyId }) {
|
|
|
244
252
|
* the validated tree is the pushed tree (base-sync's merge commit included),
|
|
245
253
|
* and a cheap conflict is found before the expensive gates.
|
|
246
254
|
*
|
|
255
|
+
* @param {object} ctx
|
|
256
|
+
* @param {object} deps
|
|
247
257
|
* @returns {Promise<{
|
|
248
258
|
* validationGates: Record<string, string>|null,
|
|
249
259
|
* lockWait: { waitedSeconds: number, expired: boolean }|null,
|
|
@@ -251,49 +261,37 @@ function resolveWorktreePath({ cwd, config, storyId }) {
|
|
|
251
261
|
* }>} `validationGates` is null when skipped; `pending` means a full-suite
|
|
252
262
|
* lock wait expired and the gates deferred.
|
|
253
263
|
*/
|
|
254
|
-
async function runPrePushPhases({
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
baseBranch,
|
|
259
|
-
baseConfirmed,
|
|
260
|
-
storyBranch,
|
|
261
|
-
storyId,
|
|
262
|
-
provider,
|
|
263
|
-
skipValidation,
|
|
264
|
-
skipSync,
|
|
265
|
-
injectedSync,
|
|
266
|
-
injectedGitSpawn,
|
|
267
|
-
setPhase = () => {},
|
|
268
|
-
setObservedGates = () => {},
|
|
269
|
-
}) {
|
|
264
|
+
async function runPrePushPhases(ctx, deps) {
|
|
265
|
+
const { options, worktreePath, baseBranch, storyBranch, storyId, setPhase } =
|
|
266
|
+
ctx;
|
|
267
|
+
const { cwd } = options;
|
|
270
268
|
setPhase('wrong-tree-guard');
|
|
271
269
|
await runWrongTreeGuardPhase({
|
|
272
270
|
cwd,
|
|
273
271
|
worktreePath,
|
|
274
272
|
baseBranch,
|
|
275
273
|
storyId,
|
|
276
|
-
provider,
|
|
274
|
+
provider: deps.provider,
|
|
277
275
|
progress,
|
|
278
|
-
gitSpawn:
|
|
276
|
+
gitSpawn: deps.gitSpawn,
|
|
279
277
|
});
|
|
280
|
-
if (!skipSync) {
|
|
278
|
+
if (!options.skipSync) {
|
|
281
279
|
setPhase('base-sync');
|
|
282
280
|
await runBaseSyncPhase({
|
|
283
281
|
cwd,
|
|
284
282
|
worktreePath,
|
|
285
283
|
baseBranch,
|
|
286
|
-
baseConfirmed,
|
|
284
|
+
baseConfirmed: ctx.baseConfirmed,
|
|
287
285
|
storyBranch,
|
|
288
286
|
storyId,
|
|
289
|
-
provider,
|
|
290
|
-
injectedSync,
|
|
287
|
+
provider: deps.provider,
|
|
288
|
+
injectedSync: deps.sync,
|
|
291
289
|
progress,
|
|
292
290
|
});
|
|
293
291
|
} else {
|
|
294
292
|
progress('SYNC', '⏭ Skipped (--skip-sync).');
|
|
295
293
|
}
|
|
296
|
-
if (skipValidation) {
|
|
294
|
+
if (options.skipValidation) {
|
|
297
295
|
progress('VALIDATE', '⏭ Skipped (--skip-validation).');
|
|
298
296
|
return { validationGates: null, lockWait: null, pending: false };
|
|
299
297
|
}
|
|
@@ -303,7 +301,7 @@ async function runPrePushPhases({
|
|
|
303
301
|
validation = await runCloseValidationPhase({
|
|
304
302
|
cwd,
|
|
305
303
|
worktreePath,
|
|
306
|
-
config,
|
|
304
|
+
config: deps.config,
|
|
307
305
|
baseBranch,
|
|
308
306
|
storyBranch,
|
|
309
307
|
storyId,
|
|
@@ -314,12 +312,12 @@ async function runPrePushPhases({
|
|
|
314
312
|
} catch (err) {
|
|
315
313
|
// The gate that died is all this run observed; the envelope claims no more.
|
|
316
314
|
if (typeof err?.closeGate === 'string') {
|
|
317
|
-
setObservedGates({ [err.closeGate]: 'failed' });
|
|
315
|
+
ctx.setObservedGates({ [err.closeGate]: 'failed' });
|
|
318
316
|
}
|
|
319
317
|
throw err;
|
|
320
318
|
}
|
|
321
319
|
const gates = validation?.gates ?? null;
|
|
322
|
-
setObservedGates(gates);
|
|
320
|
+
ctx.setObservedGates(gates);
|
|
323
321
|
return {
|
|
324
322
|
validationGates: gates,
|
|
325
323
|
lockWait: validation?.lockWait ?? null,
|
|
@@ -327,25 +325,64 @@ async function runPrePushPhases({
|
|
|
327
325
|
};
|
|
328
326
|
}
|
|
329
327
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
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');
|
|
345
382
|
// Push from the worktree so `pre-push` measures the tree being sent; the
|
|
346
383
|
// ref-based reads below resolve identically from the shared `.git`.
|
|
347
384
|
pushStoryBranch({ cwd, worktreePath, storyBranch, gitSync, progress });
|
|
348
|
-
setPhase('pull-request');
|
|
385
|
+
ctx.setPhase('pull-request');
|
|
349
386
|
const { url: prUrl, alreadyMerged } = await ensurePullRequestWith({
|
|
350
387
|
cwd,
|
|
351
388
|
storyId,
|
|
@@ -353,7 +390,7 @@ async function openAndReviewPr({
|
|
|
353
390
|
storyBody: story.body,
|
|
354
391
|
storyBranch,
|
|
355
392
|
baseBranch,
|
|
356
|
-
gh:
|
|
393
|
+
gh: deps.gh,
|
|
357
394
|
progress,
|
|
358
395
|
});
|
|
359
396
|
const prNumber = parsePrNumber(prUrl);
|
|
@@ -361,7 +398,7 @@ async function openAndReviewPr({
|
|
|
361
398
|
if (alreadyMerged) {
|
|
362
399
|
return { prUrl, prNumber, alreadyMerged: true };
|
|
363
400
|
}
|
|
364
|
-
setPhase('code-review');
|
|
401
|
+
ctx.setPhase('code-review');
|
|
365
402
|
const reviewOutcome = await runStoryScopeReview({
|
|
366
403
|
cwd,
|
|
367
404
|
storyId,
|
|
@@ -369,56 +406,24 @@ async function openAndReviewPr({
|
|
|
369
406
|
baseBranch,
|
|
370
407
|
prUrl,
|
|
371
408
|
prNumber,
|
|
372
|
-
provider,
|
|
373
|
-
runCodeReviewFn:
|
|
409
|
+
provider: deps.provider,
|
|
410
|
+
runCodeReviewFn: deps.runCodeReview,
|
|
374
411
|
gitSpawnFn: gitSpawn,
|
|
375
412
|
progress,
|
|
376
413
|
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const override = await handleOverriddenReviewBlock({
|
|
382
|
-
provider,
|
|
383
|
-
storyId,
|
|
384
|
-
prUrl,
|
|
385
|
-
prNumber,
|
|
386
|
-
criticalCount,
|
|
387
|
-
reason: overrideReviewBlock,
|
|
388
|
-
config,
|
|
389
|
-
});
|
|
390
|
-
return {
|
|
391
|
-
prUrl,
|
|
392
|
-
prNumber,
|
|
393
|
-
alreadyMerged: false,
|
|
394
|
-
reviewOverride: override,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
await handleCriticalReviewBlock({
|
|
398
|
-
provider,
|
|
399
|
-
storyId,
|
|
400
|
-
prUrl,
|
|
401
|
-
criticalCount,
|
|
402
|
-
});
|
|
403
|
-
throw new Error(
|
|
404
|
-
`[single-story-close] Story-scope review reported ${criticalCount} critical blocker(s) on PR ${prUrl}. ` +
|
|
405
|
-
'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/mandrel-deliver`. ' +
|
|
406
|
-
'If you have reviewed a finding and judged it wrong, re-run with ' +
|
|
407
|
-
'`--override-review-block "<reason>"` rather than merging by hand.',
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
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 };
|
|
411
418
|
}
|
|
412
419
|
|
|
413
|
-
async function releaseLease({
|
|
414
|
-
provider,
|
|
415
|
-
storyId,
|
|
416
|
-
config,
|
|
417
|
-
injectedReleaseLease,
|
|
418
|
-
}) {
|
|
420
|
+
async function releaseLease({ storyId }, deps) {
|
|
419
421
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
+
const outcome = await deps.releaseLease({
|
|
423
|
+
provider: deps.provider,
|
|
424
|
+
storyId,
|
|
425
|
+
config: deps.config,
|
|
426
|
+
});
|
|
422
427
|
progress(
|
|
423
428
|
'LEASE',
|
|
424
429
|
outcome.released
|
|
@@ -442,14 +447,13 @@ async function releaseLease({
|
|
|
442
447
|
*
|
|
443
448
|
* @template T
|
|
444
449
|
* @param {() => Promise<T>} run
|
|
445
|
-
* @param {{ provider: object, storyId: number, config: object, injectedReleaseLease?: Function }} leaseArgs
|
|
446
450
|
* @returns {Promise<T>}
|
|
447
451
|
*/
|
|
448
|
-
async function releaseLeaseOnBlock(run,
|
|
452
|
+
async function releaseLeaseOnBlock(run, ctx, deps) {
|
|
449
453
|
try {
|
|
450
454
|
return await run();
|
|
451
455
|
} catch (err) {
|
|
452
|
-
await releaseLease(
|
|
456
|
+
await releaseLease(ctx, deps);
|
|
453
457
|
throw err;
|
|
454
458
|
}
|
|
455
459
|
}
|
|
@@ -513,6 +517,29 @@ function closeResult({
|
|
|
513
517
|
};
|
|
514
518
|
}
|
|
515
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
|
+
|
|
516
543
|
export async function runSingleStoryClose({
|
|
517
544
|
storyId: storyIdParam,
|
|
518
545
|
cwd: cwdParam,
|
|
@@ -525,15 +552,8 @@ export async function runSingleStoryClose({
|
|
|
525
552
|
mergeWatchMode: mergeWatchModeParam,
|
|
526
553
|
rerunAdvisory: rerunAdvisoryParam,
|
|
527
554
|
overrideReviewBlock: overrideReviewBlockParam,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
injectedNotify,
|
|
531
|
-
injectedSync,
|
|
532
|
-
injectedRunCodeReview,
|
|
533
|
-
injectedGh,
|
|
534
|
-
injectedGitSpawn,
|
|
535
|
-
injectedReleaseLease,
|
|
536
|
-
injectedGraphqlProbe,
|
|
555
|
+
workerTokens: workerTokensParam,
|
|
556
|
+
...injected
|
|
537
557
|
} = {}) {
|
|
538
558
|
const options = parseCloseOptions({
|
|
539
559
|
storyIdParam,
|
|
@@ -547,10 +567,11 @@ export async function runSingleStoryClose({
|
|
|
547
567
|
mergeWatchModeParam,
|
|
548
568
|
rerunAdvisoryParam,
|
|
549
569
|
overrideReviewBlockParam,
|
|
570
|
+
workerTokensParam,
|
|
550
571
|
});
|
|
551
572
|
if (!options.storyId) {
|
|
552
573
|
throw new Error(
|
|
553
|
-
'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>]',
|
|
554
575
|
);
|
|
555
576
|
}
|
|
556
577
|
|
|
@@ -568,21 +589,11 @@ export async function runSingleStoryClose({
|
|
|
568
589
|
observedGates = gates;
|
|
569
590
|
};
|
|
570
591
|
try {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
setPhase,
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
injectedProvider,
|
|
577
|
-
injectedConfig,
|
|
578
|
-
injectedNotify,
|
|
579
|
-
injectedSync,
|
|
580
|
-
injectedRunCodeReview,
|
|
581
|
-
injectedGh,
|
|
582
|
-
injectedGitSpawn,
|
|
583
|
-
injectedReleaseLease,
|
|
584
|
-
injectedGraphqlProbe,
|
|
585
|
-
});
|
|
592
|
+
const startedAtMs = Date.now();
|
|
593
|
+
return await runClosePipeline(
|
|
594
|
+
{ options, setPhase, setObservedGates, phaseTimer, startedAtMs },
|
|
595
|
+
resolveCloseDeps(options, injected),
|
|
596
|
+
);
|
|
586
597
|
} catch (err) {
|
|
587
598
|
if (err && typeof err === 'object') {
|
|
588
599
|
err.closePhaseDurations = phaseTimer.finish();
|
|
@@ -706,6 +717,7 @@ async function finishWithMergeWait(prCtx, deps) {
|
|
|
706
717
|
result,
|
|
707
718
|
config: prCtx.config,
|
|
708
719
|
phaseTimer: prCtx.phaseTimer,
|
|
720
|
+
workerTokens: prCtx.workerTokens,
|
|
709
721
|
});
|
|
710
722
|
reportWaitTerminal(terminal, { storyId: prCtx.storyId, prUrl: prCtx.prUrl });
|
|
711
723
|
return { success: terminal.status === 'landed', result, terminal };
|
|
@@ -759,6 +771,7 @@ async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
|
759
771
|
result,
|
|
760
772
|
config: prCtx.config,
|
|
761
773
|
phaseTimer: prCtx.phaseTimer,
|
|
774
|
+
workerTokens: prCtx.workerTokens,
|
|
762
775
|
});
|
|
763
776
|
progress(
|
|
764
777
|
'DONE',
|
|
@@ -777,14 +790,14 @@ async function finishWithoutMergeWait(prCtx, waitForMergeReason) {
|
|
|
777
790
|
*/
|
|
778
791
|
async function finishDeferred(
|
|
779
792
|
lockWait,
|
|
780
|
-
{ config, startedAtMs, phaseTimer, ...ids },
|
|
793
|
+
{ config, startedAtMs, phaseTimer, workerTokens, ...ids },
|
|
781
794
|
) {
|
|
782
795
|
const { result, terminal, note } = lockWaitPending({
|
|
783
796
|
...ids,
|
|
784
797
|
lockWait,
|
|
785
798
|
elapsedSeconds: elapsedSecondsSince(startedAtMs),
|
|
786
799
|
});
|
|
787
|
-
await emitTerminal({ terminal, result, config, phaseTimer });
|
|
800
|
+
await emitTerminal({ terminal, result, config, phaseTimer, workerTokens });
|
|
788
801
|
progress('PENDING', note);
|
|
789
802
|
return { success: false, result, terminal };
|
|
790
803
|
}
|
|
@@ -819,212 +832,212 @@ function reportOperatorMergeSkip({
|
|
|
819
832
|
);
|
|
820
833
|
}
|
|
821
834
|
|
|
822
|
-
async function
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
injectedGitSpawn,
|
|
834
|
-
injectedReleaseLease,
|
|
835
|
-
injectedGraphqlProbe,
|
|
836
|
-
}) {
|
|
837
|
-
const startedAtMs = Date.now();
|
|
838
|
-
const config = injectedConfig || resolveConfig({ cwd: options.cwd });
|
|
839
|
-
const provider = injectedProvider || createProvider(config);
|
|
840
|
-
const storyBranch = getStoryBranch(options.storyId);
|
|
841
|
-
|
|
842
|
-
progress('INIT', `Closing standalone Story #${options.storyId}...`);
|
|
843
|
-
const story = await provider.getTicket(options.storyId);
|
|
844
|
-
if (story.state === 'closed') {
|
|
845
|
-
return await alreadyClosedResult(
|
|
846
|
-
options.storyId,
|
|
847
|
-
story.stateReason,
|
|
848
|
-
config,
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const leaseArgs = {
|
|
853
|
-
provider,
|
|
854
|
-
storyId: options.storyId,
|
|
855
|
-
config,
|
|
856
|
-
injectedReleaseLease,
|
|
857
|
-
};
|
|
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
|
+
}
|
|
858
846
|
|
|
859
|
-
|
|
847
|
+
/** One cheap GraphQL read here beats the gate chain plus a push. */
|
|
848
|
+
async function graphqlPreflightPhase(ctx, deps) {
|
|
860
849
|
const preflight = await runGraphqlPreflight({
|
|
861
|
-
storyId:
|
|
862
|
-
provider,
|
|
850
|
+
storyId: ctx.storyId,
|
|
851
|
+
provider: deps.provider,
|
|
863
852
|
progress,
|
|
864
|
-
ghFacade:
|
|
865
|
-
probe:
|
|
853
|
+
ghFacade: deps.gh,
|
|
854
|
+
probe: deps.graphqlProbe,
|
|
866
855
|
});
|
|
867
|
-
if (preflight)
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
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
|
+
}
|
|
876
866
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
+
}
|
|
886
886
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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,
|
|
891
902
|
});
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
storyBranch,
|
|
900
|
-
provider,
|
|
901
|
-
worktreePath,
|
|
902
|
-
injectedSync,
|
|
903
|
-
injectedGitSpawn,
|
|
904
|
-
setPhase,
|
|
905
|
-
setObservedGates,
|
|
906
|
-
}),
|
|
907
|
-
leaseArgs,
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function openPrPhase(ctx, deps) {
|
|
906
|
+
ctx.pr = await releaseLeaseOnBlock(
|
|
907
|
+
() => openAndReviewPr(ctx, deps),
|
|
908
|
+
ctx,
|
|
909
|
+
deps,
|
|
908
910
|
);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
storyId: options.storyId,
|
|
912
|
-
storyBranch,
|
|
913
|
-
baseBranch,
|
|
914
|
-
config,
|
|
915
|
-
startedAtMs,
|
|
916
|
-
phaseTimer,
|
|
917
|
-
});
|
|
918
|
-
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
919
913
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
config,
|
|
932
|
-
overrideReviewBlock: options.overrideReviewBlock,
|
|
933
|
-
injectedGh,
|
|
934
|
-
injectedRunCodeReview,
|
|
935
|
-
setPhase,
|
|
936
|
-
}),
|
|
937
|
-
leaseArgs,
|
|
938
|
-
);
|
|
939
|
-
// Reap BEFORE the arm: `gh pr merge --delete-branch` may merge at once and
|
|
940
|
-
// then fail deleting a branch a live worktree holds, reading as a failed
|
|
941
|
-
// arm. Safe — the work is pushed, and a dirty tree is still refused.
|
|
942
|
-
const worktreeReaped = await reapWorktreePhase({
|
|
943
|
-
cwd: options.cwd,
|
|
944
|
-
storyId: options.storyId,
|
|
945
|
-
worktreePath,
|
|
946
|
-
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,
|
|
947
925
|
progress,
|
|
948
926
|
WorktreeManager,
|
|
949
927
|
});
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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({
|
|
959
937
|
alreadyMerged,
|
|
960
|
-
cwd: options.cwd,
|
|
938
|
+
cwd: ctx.options.cwd,
|
|
961
939
|
prNumber,
|
|
962
940
|
prUrl,
|
|
963
|
-
noAutoMerge: options.noAutoMerge,
|
|
941
|
+
noAutoMerge: ctx.options.noAutoMerge,
|
|
964
942
|
autoMergePolicy: ciDelivery.autoMerge,
|
|
965
943
|
blockOnAdvisoryFailure: ciDelivery.blockOnAdvisoryFailure,
|
|
966
944
|
advisoryAllowlist: ciDelivery.advisoryAllowlist,
|
|
967
|
-
gh:
|
|
945
|
+
gh: deps.gh,
|
|
968
946
|
progress,
|
|
969
947
|
});
|
|
970
948
|
await flipLabelAndNotify({
|
|
971
|
-
provider,
|
|
972
|
-
notifyFn:
|
|
973
|
-
storyId:
|
|
974
|
-
story,
|
|
949
|
+
provider: deps.provider,
|
|
950
|
+
notifyFn: deps.notify,
|
|
951
|
+
storyId: ctx.storyId,
|
|
952
|
+
story: ctx.story,
|
|
975
953
|
prUrl,
|
|
976
|
-
autoMergeEnabled,
|
|
977
|
-
autoMergeReason,
|
|
978
|
-
config,
|
|
954
|
+
autoMergeEnabled: ctx.arm.autoMergeEnabled,
|
|
955
|
+
autoMergeReason: ctx.arm.autoMergeReason,
|
|
956
|
+
config: deps.config,
|
|
979
957
|
progress,
|
|
980
958
|
});
|
|
981
|
-
|
|
982
|
-
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
983
961
|
|
|
984
|
-
|
|
985
|
-
|
|
962
|
+
/** Wait-for-merge needs the arm outcome, so it is resolved here. */
|
|
963
|
+
async function finishPhase(ctx, deps) {
|
|
964
|
+
const { options, arm } = ctx;
|
|
986
965
|
const { waitForMerge, reason: waitForMergeReason } = resolveWaitForMerge({
|
|
987
966
|
waitForMergeExplicit: options.waitForMergeExplicit,
|
|
988
967
|
noWaitForMerge: options.noWaitForMerge,
|
|
989
|
-
config,
|
|
990
|
-
autoMergeReason,
|
|
968
|
+
config: deps.config,
|
|
969
|
+
autoMergeReason: arm.autoMergeReason,
|
|
991
970
|
});
|
|
992
971
|
reportOperatorMergeSkip({
|
|
993
972
|
waitForMergeReason,
|
|
994
|
-
autoMergeReason,
|
|
995
|
-
storyId:
|
|
973
|
+
autoMergeReason: arm.autoMergeReason,
|
|
974
|
+
storyId: ctx.storyId,
|
|
996
975
|
waitForMergeExplicit: options.waitForMergeExplicit,
|
|
997
976
|
});
|
|
998
977
|
const prCtx = {
|
|
999
|
-
storyId:
|
|
1000
|
-
storyBranch,
|
|
1001
|
-
baseBranch,
|
|
1002
|
-
prNumber,
|
|
1003
|
-
prUrl,
|
|
1004
|
-
autoMergeEnabled,
|
|
1005
|
-
autoMergeReason,
|
|
1006
|
-
advisoryGate,
|
|
1007
|
-
worktreeReaped,
|
|
1008
|
-
localCleanupDeferred,
|
|
1009
|
-
directMerged,
|
|
1010
|
-
config,
|
|
1011
|
-
startedAtMs,
|
|
1012
|
-
phaseTimer,
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
+
),
|
|
1015
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
|
+
}
|
|
1016
1014
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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;
|
|
1028
1042
|
}
|
|
1029
|
-
return await finishWithoutMergeWait(prCtx, waitForMergeReason);
|
|
1030
1043
|
}
|