mandrel 2.35.0 → 2.37.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/docs/agentrc-reference.json +3 -1
- package/.agents/docs/configuration.md +27 -0
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/schemas/agentrc.schema.json +11 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/boot-sweep.js +7 -1
- package/.agents/scripts/check-context-budget.js +195 -103
- package/.agents/scripts/check-cyclomatic.js +112 -42
- package/.agents/scripts/check-doc-links.js +23 -2
- package/.agents/scripts/check-generated-validator.js +202 -0
- package/.agents/scripts/check-lifecycle-lint.js +2 -68
- package/.agents/scripts/check-schema-references.js +2 -28
- package/.agents/scripts/drain-pending-cleanup.js +18 -1
- package/.agents/scripts/git-cleanup.js +2 -0
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
- package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
- package/.agents/scripts/lib/checks/loop-health.js +1 -1
- package/.agents/scripts/lib/cli-args.js +0 -2
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/config/ci.js +18 -0
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +13 -0
- package/.agents/scripts/lib/config-settings-schema.js +46 -6
- package/.agents/scripts/lib/coverage-baseline.js +2 -2
- package/.agents/scripts/lib/cpu-pool.js +90 -10
- package/.agents/scripts/lib/crap-utils.js +6 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
- package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
- package/.agents/scripts/lib/dependency-version.js +86 -0
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/errors/index.js +1 -1
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +22 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -14
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/merged-tip.js +132 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +56 -11
- package/.agents/scripts/lib/orchestration/merge-block-class.js +10 -1
- package/.agents/scripts/lib/orchestration/merge-poll.js +164 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
- package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +145 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +9 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -1
- package/.agents/scripts/lib/preflight-runner.js +3 -3
- package/.agents/scripts/lib/qa/qa-session.js +1 -1
- package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
- package/.agents/scripts/lib/test-isolate/runner.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +14 -6
- package/.agents/scripts/lib/test-tiers.js +135 -18
- package/.agents/scripts/lib/transpile.js +16 -3
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/notify.js +4 -10
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/tickets.js +109 -16
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +0 -2
- package/.agents/scripts/single-story-init.js +5 -1
- package/.agents/workflows/audit-documentation.md +5 -6
- package/docs/CHANGELOG.md +50 -0
- package/lib/cli/migrate.js +100 -94
- package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
- package/lib/migrations/index.js +32 -33
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
- package/package.json +9 -5
- package/.agents/scripts/generate-lifecycle-docs.js +0 -237
- package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
- package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
- package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
- package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
- package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
- package/.agents/scripts/lib/orchestration/recut.js +0 -56
|
@@ -96,7 +96,15 @@ export const BLOCK_CLASSES = Object.freeze([
|
|
|
96
96
|
* This is the classifier's four outputs PLUS `predicate-refused` (#4472),
|
|
97
97
|
* emitted DIRECTLY for a headless refusal that never reached the
|
|
98
98
|
* poll-exhaustion classifier — so it is a valid attribution value even though
|
|
99
|
-
* `classifyMergeBlock` never produces it.
|
|
99
|
+
* `classifyMergeBlock` never produces it. Story #5096 added
|
|
100
|
+
* `advisory-gate-red` on the same footing: a genuinely red NON-required check
|
|
101
|
+
* observed while GitHub reports the PR mergeable anyway
|
|
102
|
+
* (`mergeStateStatus: UNSTABLE`), which native auto-merge would land straight
|
|
103
|
+
* past. It is emitted directly by the arm and merge-wait phases — the
|
|
104
|
+
* classifier cannot produce it, because by construction GitHub is NOT blocking
|
|
105
|
+
* the merge, which is the entire problem it names. It is deliberately NOT in
|
|
106
|
+
* `BLOCK_CLASSES`, whose reachability invariant covers only what
|
|
107
|
+
* `classifyMergeBlock` returns. (The Epic-era listeners that used to
|
|
100
108
|
* emit it, AutomergePredicate and AutomergeArmer, are gone; the value stays
|
|
101
109
|
* because archived `merge.unlanded` records carry it and the schema enum
|
|
102
110
|
* must keep validating them.) `isValidBlockClass` (and the `merge.unlanded` schema enum)
|
|
@@ -106,6 +114,7 @@ export const BLOCK_CLASSES = Object.freeze([
|
|
|
106
114
|
export const MERGE_UNLANDED_BLOCK_CLASSES = Object.freeze([
|
|
107
115
|
...BLOCK_CLASSES,
|
|
108
116
|
'predicate-refused',
|
|
117
|
+
'advisory-gate-red',
|
|
109
118
|
]);
|
|
110
119
|
|
|
111
120
|
const BLOCK_CLASS_SET = new Set(MERGE_UNLANDED_BLOCK_CLASSES);
|
|
@@ -243,6 +243,170 @@ export function requiredCheckFailedBlocksMerge(prProbe) {
|
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* The one `mergeStateStatus` value that means the PR is mergeable **despite**
|
|
248
|
+
* red runs — GitHub's own words are "mergeable with non-passing commit
|
|
249
|
+
* status". It is the required-vs-advisory discriminator the rollup itself
|
|
250
|
+
* cannot supply: under `UNSTABLE` the red runs are, by definition, not
|
|
251
|
+
* required, so native auto-merge will land the PR over them.
|
|
252
|
+
*
|
|
253
|
+
* The exact complement of {@link MERGE_GATED_STATE}: `BLOCKED` means the red
|
|
254
|
+
* run gates the merge (`failingChecksBlockMerge`), `UNSTABLE` means it does
|
|
255
|
+
* not and only mandrel can stop the landing.
|
|
256
|
+
*/
|
|
257
|
+
const MERGE_ADVISORY_STATE = 'UNSTABLE';
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Pure: project the HEAD-ANCHORED runs that genuinely concluded red, naming
|
|
261
|
+
* each one (Story #5096).
|
|
262
|
+
*
|
|
263
|
+
* Same red-ness test as {@link deriveRequiredRunEvidence} — `FAILURE` /
|
|
264
|
+
* `ERROR` only, never `CANCELLED` / `TIMED_OUT` / `SKIPPED`, which are the
|
|
265
|
+
* superseded-push and sibling-invalidated runs a bare rollup read miscounts
|
|
266
|
+
* (the #4695 / #4710 trap) — but it returns the runs rather than a boolean, so
|
|
267
|
+
* a block summary can name the offending job and the advisory allowlist can
|
|
268
|
+
* match on it.
|
|
269
|
+
*
|
|
270
|
+
* `name` is the CheckRun's `name`, falling back to a legacy StatusContext's
|
|
271
|
+
* `context`, and is `null` when the projection carries neither. A run with no
|
|
272
|
+
* readable name can never match an allowlist entry, so it always blocks — the
|
|
273
|
+
* conservative direction for a gate whose whole purpose is to stop a silent
|
|
274
|
+
* landing.
|
|
275
|
+
*
|
|
276
|
+
* @param {Array<{name?: string, context?: string, status?: string, conclusion?: string, state?: string}>} statusCheckRollup
|
|
277
|
+
* @returns {Array<{ name: string|null, conclusion: string }>}
|
|
278
|
+
*/
|
|
279
|
+
export function deriveRedHeadRuns(statusCheckRollup) {
|
|
280
|
+
if (!Array.isArray(statusCheckRollup)) return [];
|
|
281
|
+
const red = [];
|
|
282
|
+
for (const check of statusCheckRollup) {
|
|
283
|
+
const conclusion = String(check?.conclusion ?? '').toUpperCase();
|
|
284
|
+
const state = String(check?.state ?? '').toUpperCase();
|
|
285
|
+
const isRed =
|
|
286
|
+
conclusion === 'FAILURE' ||
|
|
287
|
+
conclusion === 'ERROR' ||
|
|
288
|
+
state === 'FAILURE' ||
|
|
289
|
+
state === 'ERROR';
|
|
290
|
+
if (!isRed) continue;
|
|
291
|
+
const name =
|
|
292
|
+
typeof check?.name === 'string' && check.name
|
|
293
|
+
? check.name
|
|
294
|
+
: typeof check?.context === 'string' && check.context
|
|
295
|
+
? check.context
|
|
296
|
+
: null;
|
|
297
|
+
red.push({ name, conclusion: conclusion || state });
|
|
298
|
+
}
|
|
299
|
+
return red;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Pure: drop the red runs a consumer has exempted via
|
|
304
|
+
* `delivery.ci.advisoryAllowlist`, returning the ones that still block.
|
|
305
|
+
*
|
|
306
|
+
* Matching is exact on the run name. An unnamed run never matches (see
|
|
307
|
+
* {@link deriveRedHeadRuns}).
|
|
308
|
+
*
|
|
309
|
+
* @param {Array<{ name: string|null, conclusion: string }>} redHeadRuns
|
|
310
|
+
* @param {string[]} [allowlist]
|
|
311
|
+
* @returns {Array<{ name: string|null, conclusion: string }>}
|
|
312
|
+
*/
|
|
313
|
+
export function selectBlockingRedRuns(redHeadRuns, allowlist = []) {
|
|
314
|
+
if (!Array.isArray(redHeadRuns) || redHeadRuns.length === 0) return [];
|
|
315
|
+
const exempt = new Set(
|
|
316
|
+
(Array.isArray(allowlist) ? allowlist : [])
|
|
317
|
+
.filter((entry) => typeof entry === 'string' && entry)
|
|
318
|
+
.map((entry) => entry),
|
|
319
|
+
);
|
|
320
|
+
if (exempt.size === 0) return [...redHeadRuns];
|
|
321
|
+
return redHeadRuns.filter((run) => !(run?.name && exempt.has(run.name)));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Pure: does this PR carry a genuinely red ADVISORY run — one that will NOT
|
|
326
|
+
* stop GitHub from landing the PR, and therefore one only mandrel can act on?
|
|
327
|
+
* (Story #5096.)
|
|
328
|
+
*
|
|
329
|
+
* The complement of {@link requiredCheckFailedBlocksMerge}. That predicate
|
|
330
|
+
* answers "is a red REQUIRED check gating the merge" (`BLOCKED`); this one
|
|
331
|
+
* answers "is a red NON-required check about to be merged straight past"
|
|
332
|
+
* (`UNSTABLE`). The two are mutually exclusive by construction, so a red
|
|
333
|
+
* required check keeps its existing `checks-failed` treatment untouched.
|
|
334
|
+
*
|
|
335
|
+
* **Fails OPEN by design.** `UNKNOWN`, `CLEAN`, `BEHIND`, or an absent
|
|
336
|
+
* `mergeStateStatus` all return `false`. The asymmetry is the same one
|
|
337
|
+
* {@link failingChecksBlockMerge} documents and is deliberate: failing to
|
|
338
|
+
* block costs an unattended landing the operator can still revert, whereas
|
|
339
|
+
* blocking wrongly strands a mergeable PR at `agent::blocked` that only an
|
|
340
|
+
* operator can unpick. A transient `UNKNOWN` must never do the latter.
|
|
341
|
+
*
|
|
342
|
+
* @param {{ mergeStateStatus?: string, redHeadRuns?: Array<{name: string|null, conclusion: string}> }} [prProbe]
|
|
343
|
+
* @param {string[]} [allowlist] `delivery.ci.advisoryAllowlist`.
|
|
344
|
+
* @returns {boolean}
|
|
345
|
+
*/
|
|
346
|
+
export function advisoryCheckFailedBlocksArm(prProbe, allowlist = []) {
|
|
347
|
+
if (
|
|
348
|
+
String(prProbe?.mergeStateStatus ?? '').toUpperCase() !==
|
|
349
|
+
MERGE_ADVISORY_STATE
|
|
350
|
+
) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return selectBlockingRedRuns(prProbe?.redHeadRuns, allowlist).length > 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Pure: the merge wait's advisory-gate decision, the sibling of
|
|
358
|
+
* {@link decideMergeWaitFailFast} (Story #5096).
|
|
359
|
+
*
|
|
360
|
+
* Encapsulates the whole policy — the knob, the predicate, and the allowlist
|
|
361
|
+
* projection — so the poll body carries a single assignment rather than three
|
|
362
|
+
* decision points. `runMergePoll` sits above `check-cyclomatic`'s ceiling
|
|
363
|
+
* already; every branch added inline there is a real regression, and this
|
|
364
|
+
* policy has a natural home beside the predicate it consumes.
|
|
365
|
+
*
|
|
366
|
+
* Returns `null` when the wait should keep polling — the knob is off, the PR
|
|
367
|
+
* is not in the advisory-red state, or every red run is allowlisted.
|
|
368
|
+
*
|
|
369
|
+
* @param {object} args
|
|
370
|
+
* @returns {{ blockingRuns: Array<object>, reason: string } | null}
|
|
371
|
+
*/
|
|
372
|
+
export function decideAdvisoryGateBlock({
|
|
373
|
+
probe,
|
|
374
|
+
blockOnAdvisoryFailure,
|
|
375
|
+
advisoryAllowlist,
|
|
376
|
+
}) {
|
|
377
|
+
if (!blockOnAdvisoryFailure) return null;
|
|
378
|
+
if (!advisoryCheckFailedBlocksArm(probe, advisoryAllowlist)) return null;
|
|
379
|
+
const blockingRuns = selectBlockingRedRuns(
|
|
380
|
+
probe?.redHeadRuns,
|
|
381
|
+
advisoryAllowlist,
|
|
382
|
+
);
|
|
383
|
+
return { blockingRuns, reason: formatAdvisoryGateReason(blockingRuns) };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Format the one-line reason a `merge.unlanded` record and the operator-facing
|
|
388
|
+
* block carry for an `advisory-gate-red` verdict, naming each offending job
|
|
389
|
+
* and its conclusion.
|
|
390
|
+
*
|
|
391
|
+
* @param {Array<{ name: string|null, conclusion: string }>} blockingRuns
|
|
392
|
+
* @returns {string}
|
|
393
|
+
*/
|
|
394
|
+
export function formatAdvisoryGateReason(blockingRuns) {
|
|
395
|
+
const named = (Array.isArray(blockingRuns) ? blockingRuns : [])
|
|
396
|
+
.map(
|
|
397
|
+
(run) =>
|
|
398
|
+
`${run?.name ?? '(unnamed run)'} → ${run?.conclusion ?? 'FAILURE'}`,
|
|
399
|
+
)
|
|
400
|
+
.join(', ');
|
|
401
|
+
return (
|
|
402
|
+
'A non-required (advisory) check concluded red on the PR head, and GitHub ' +
|
|
403
|
+
'reports the PR mergeable anyway (mergeStateStatus=UNSTABLE) — native ' +
|
|
404
|
+
'auto-merge would land it over the failure. Red advisory job(s): ' +
|
|
405
|
+
`${named || '(none named)'}. Merge by hand to land over it deliberately, ` +
|
|
406
|
+
'or exempt the job via delivery.ci.advisoryAllowlist.'
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
246
410
|
/**
|
|
247
411
|
* Pure: the merge wait's single fail-fast decision (Story #4710 — extracted
|
|
248
412
|
* from the two near-verbatim inline blocks in `runConfirmMergePhase`'s poll
|
|
@@ -197,6 +197,26 @@ function planFingerprintMarker(fingerprint) {
|
|
|
197
197
|
return `<!-- ${PLAN_FINGERPRINT_MARKER_PREFIX} ${fingerprint} -->`;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/** The one parser for the marker {@link planFingerprintMarker} renders. */
|
|
201
|
+
const PLAN_FINGERPRINT_MARKER_RE = new RegExp(
|
|
202
|
+
`<!--\\s*${PLAN_FINGERPRINT_MARKER_PREFIX}\\s*([0-9a-f]+)\\s*-->`,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Recover the plan fingerprint an issue body was stamped with, or `null` when
|
|
207
|
+
* the body carries no marker. Module-private: both readers of the marker (the
|
|
208
|
+
* resume index and the create-retry adoption probe) go through it so they can
|
|
209
|
+
* never drift into recognising different Stories as "already created".
|
|
210
|
+
*
|
|
211
|
+
* @param {unknown} body
|
|
212
|
+
* @returns {string|null}
|
|
213
|
+
*/
|
|
214
|
+
function extractPlanFingerprint(body) {
|
|
215
|
+
if (typeof body !== 'string') return null;
|
|
216
|
+
const match = body.match(PLAN_FINGERPRINT_MARKER_RE);
|
|
217
|
+
return match ? match[1] : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
200
220
|
/**
|
|
201
221
|
* Labels the authoring pass is never allowed to set. The `agent::*` axis is
|
|
202
222
|
* the runtime's lifecycle state (persist owns the terminal `agent::ready`
|
|
@@ -617,14 +637,9 @@ async function indexExistingStories(provider) {
|
|
|
617
637
|
if (title !== '') {
|
|
618
638
|
idsByTitle.set(title, [...(idsByTitle.get(title) ?? []), id]);
|
|
619
639
|
}
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
`<!--\\s*${PLAN_FINGERPRINT_MARKER_PREFIX}\\s*([0-9a-f]+)\\s*-->`,
|
|
624
|
-
),
|
|
625
|
-
);
|
|
626
|
-
if (!match) continue;
|
|
627
|
-
byFingerprint.set(match[1], {
|
|
640
|
+
const fingerprint = extractPlanFingerprint(issue?.body);
|
|
641
|
+
if (!fingerprint) continue;
|
|
642
|
+
byFingerprint.set(fingerprint, {
|
|
628
643
|
id,
|
|
629
644
|
title,
|
|
630
645
|
url: issue.html_url ?? issue.url ?? undefined,
|
|
@@ -633,6 +648,42 @@ async function indexExistingStories(provider) {
|
|
|
633
648
|
return { byFingerprint, idsByTitle };
|
|
634
649
|
}
|
|
635
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Re-run the resume lookup for a single fingerprint and return the **raw**
|
|
653
|
+
* issue, or `null`.
|
|
654
|
+
*
|
|
655
|
+
* This is the probe `createIssue` calls before any retry POST (Story #5112).
|
|
656
|
+
* A create whose response was lost has already filed the issue; retrying
|
|
657
|
+
* blind duplicates it. Because the body posted carries the fingerprint
|
|
658
|
+
* marker, the same content-keyed lookup the resume path uses answers "did
|
|
659
|
+
* attempt 1 land?" authoritatively — from the server's state, not from a
|
|
660
|
+
* client-side guess about where the connection broke.
|
|
661
|
+
*
|
|
662
|
+
* Best-effort like {@link indexExistingStories}: a provider without the
|
|
663
|
+
* listing surface, or a listing that throws, yields `null` and the retry
|
|
664
|
+
* proceeds exactly as it did before.
|
|
665
|
+
*
|
|
666
|
+
* @param {{ provider: object, fingerprint: string }} args
|
|
667
|
+
* @returns {Promise<object|null>}
|
|
668
|
+
*/
|
|
669
|
+
async function findOpenStoryByPlanFingerprint({ provider, fingerprint }) {
|
|
670
|
+
if (typeof provider?.listIssuesByLabel !== 'function') return null;
|
|
671
|
+
if (typeof fingerprint !== 'string' || fingerprint.length === 0) return null;
|
|
672
|
+
let issues;
|
|
673
|
+
try {
|
|
674
|
+
issues = await provider.listIssuesByLabel({
|
|
675
|
+
state: 'open',
|
|
676
|
+
labels: TYPE_LABELS.STORY,
|
|
677
|
+
});
|
|
678
|
+
} catch {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
for (const issue of Array.isArray(issues) ? issues : []) {
|
|
682
|
+
if (extractPlanFingerprint(issue?.body) === fingerprint) return issue;
|
|
683
|
+
}
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
636
687
|
/**
|
|
637
688
|
* Warn when a Story with this title is already open but did **not** match the
|
|
638
689
|
* fingerprint — i.e. its authored content differs from what this run is about
|
|
@@ -1000,6 +1051,14 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1000
1051
|
...(applyCohortLabel ? [cohortLabel] : []),
|
|
1001
1052
|
...(applyRouteLabel ? [routeLabel] : []),
|
|
1002
1053
|
],
|
|
1054
|
+
// Story #5112 — hand the provider the same content-keyed lookup this
|
|
1055
|
+
// loop's resume path uses, so a retry after a lost response adopts the
|
|
1056
|
+
// issue attempt 1 already filed instead of creating a twin.
|
|
1057
|
+
findExisting: () =>
|
|
1058
|
+
findOpenStoryByPlanFingerprint({
|
|
1059
|
+
provider,
|
|
1060
|
+
fingerprint: story.fingerprint,
|
|
1061
|
+
}),
|
|
1003
1062
|
});
|
|
1004
1063
|
const id = result?.id ?? result?.number;
|
|
1005
1064
|
if (!Number.isInteger(id)) {
|
|
@@ -1012,7 +1071,9 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1012
1071
|
id,
|
|
1013
1072
|
title: story.title,
|
|
1014
1073
|
url: result.url,
|
|
1015
|
-
adopted
|
|
1074
|
+
// True when the provider's retry probe adopted an issue a lost-response
|
|
1075
|
+
// first attempt had already filed — pre-existing either way.
|
|
1076
|
+
adopted: result.adopted === true,
|
|
1016
1077
|
});
|
|
1017
1078
|
idBySlug.set(story.slug, id);
|
|
1018
1079
|
}
|
|
@@ -50,35 +50,6 @@ import { applyBehindUpdate } from './behind-recovery.js';
|
|
|
50
50
|
* values collapse to `'skipped'` so any future GitHub state we haven't
|
|
51
51
|
* enumerated still maps into the vocabulary.
|
|
52
52
|
*/
|
|
53
|
-
/**
|
|
54
|
-
* The raw check-state tokens `normalizeCheckState` recognizes (lowercased).
|
|
55
|
-
* A token absent from this set is one we have NOT enumerated — the watch
|
|
56
|
-
* path collapses it to `'skipped'` (validate-anything), but a fail-closed
|
|
57
|
-
* consumer (the auto-merge arming probe) must treat it as unknown-therefore-
|
|
58
|
-
* blocking rather than trust the `'skipped'` collapse. Exported so that
|
|
59
|
-
* stricter consumer lives here as the single vocabulary owner.
|
|
60
|
-
*/
|
|
61
|
-
export const RECOGNIZED_CHECK_STATES = Object.freeze(
|
|
62
|
-
new Set([
|
|
63
|
-
'',
|
|
64
|
-
'pending',
|
|
65
|
-
'queued',
|
|
66
|
-
'in_progress',
|
|
67
|
-
'requested',
|
|
68
|
-
'waiting',
|
|
69
|
-
'success',
|
|
70
|
-
'completed',
|
|
71
|
-
'failure',
|
|
72
|
-
'startup_failure',
|
|
73
|
-
'neutral',
|
|
74
|
-
'cancelled',
|
|
75
|
-
'timed_out',
|
|
76
|
-
'action_required',
|
|
77
|
-
'stale',
|
|
78
|
-
'skipped',
|
|
79
|
-
]),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
53
|
export function normalizeCheckState(raw) {
|
|
83
54
|
const v = String(raw ?? '')
|
|
84
55
|
.trim()
|
|
@@ -325,7 +296,7 @@ export function allTerminal(outcomes) {
|
|
|
325
296
|
* red check): a `'still-running'` map means "re-arm the watch / hand off
|
|
326
297
|
* to `/loop`," never "the change is broken."
|
|
327
298
|
*/
|
|
328
|
-
|
|
299
|
+
const STILL_RUNNING = 'still-running';
|
|
329
300
|
|
|
330
301
|
/**
|
|
331
302
|
* Promote any leftover `'pending'` outcomes to the schema-valid
|
|
@@ -38,14 +38,14 @@ import { ColumnSync, columnForLabels } from './column-sync.js';
|
|
|
38
38
|
* long enough to outlast the typical GitHub built-in workflow fire
|
|
39
39
|
* timing without ballooning the close-path wall clock.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
const DEFAULT_POLL_ATTEMPTS = 4;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Default delay between drift checks (ms). Five seconds is a sweet
|
|
45
45
|
* spot: short enough that the operator doesn't perceive the close
|
|
46
46
|
* path as hung, long enough that one re-fire usually wins the race.
|
|
47
47
|
*/
|
|
48
|
-
|
|
48
|
+
const DEFAULT_POLL_DELAY_MS = 5000;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Production sleep function. Tests inject `sleepFn: () => Promise.resolve()`
|
|
@@ -57,7 +57,7 @@ export const CODEX_REMEDIATIONS = Object.freeze({
|
|
|
57
57
|
*
|
|
58
58
|
* Exported so tests can extend the list without monkey-patching `os`.
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
const DEFAULT_PLUGIN_MARKERS = Object.freeze([
|
|
61
61
|
path.join(os.homedir(), '.claude', 'plugins', 'codex-plugin-cc'),
|
|
62
62
|
path.join(os.homedir(), '.claude', 'plugins', 'openai', 'codex-plugin-cc'),
|
|
63
63
|
]);
|
|
@@ -194,7 +194,7 @@ export function parseCodexFindings(rawStdout) {
|
|
|
194
194
|
* @param {{ baseRef: string, headRef: string, depth?: import('./types.js').ReviewDepth }} args
|
|
195
195
|
* @returns {string}
|
|
196
196
|
*/
|
|
197
|
-
|
|
197
|
+
function buildCodexReviewPrompt({ baseRef, headRef, depth }) {
|
|
198
198
|
return (
|
|
199
199
|
`/codex:review --base ${baseRef} --head ${headRef} --wait ` +
|
|
200
200
|
`${renderDepthDirective(depth)}`
|
|
@@ -214,7 +214,7 @@ export function buildCodexReviewPrompt({ baseRef, headRef, depth }) {
|
|
|
214
214
|
* @param {{ baseRef: string, headRef: string, depth?: import('./types.js').ReviewDepth }} args
|
|
215
215
|
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
216
216
|
*/
|
|
217
|
-
|
|
217
|
+
function defaultInvokeCodexReview({ baseRef, headRef, depth }) {
|
|
218
218
|
const cliArgs = [
|
|
219
219
|
'--print',
|
|
220
220
|
buildCodexReviewPrompt({ baseRef, headRef, depth }),
|
|
@@ -102,7 +102,7 @@ export function renderFinding(finding) {
|
|
|
102
102
|
* @param {ReadonlyArray<string>} messages
|
|
103
103
|
* @returns {string[]} lines to append (empty when no messages)
|
|
104
104
|
*/
|
|
105
|
-
|
|
105
|
+
function renderManualPromptsSection(messages) {
|
|
106
106
|
const filtered = Array.isArray(messages)
|
|
107
107
|
? messages.filter((m) => typeof m === 'string' && m.trim().length > 0)
|
|
108
108
|
: [];
|
|
@@ -267,8 +267,14 @@ function isJsMaintainabilityFile(relPath) {
|
|
|
267
267
|
* `reportFn` forces the serial path (the injected scorer cannot cross the
|
|
268
268
|
* worker boundary).
|
|
269
269
|
*
|
|
270
|
+
* `serialThreshold` overrides the pool-vs-serial cutover for one call. It is
|
|
271
|
+
* the seam the parity test uses to drive the pooled branch: Story #5109
|
|
272
|
+
* retuned `POOL_SERIAL_THRESHOLD` from 8 to 256 against measured data, and a
|
|
273
|
+
* fixture sized to clear the old number would otherwise have gone quietly
|
|
274
|
+
* serial while still claiming to cover the pool.
|
|
275
|
+
*
|
|
270
276
|
* @param {string[]} changedFiles
|
|
271
|
-
* @param {{ reportFn?: Function, classifier?: Function, runOnPoolFn?: typeof runOnPool, headRef?: string|null, gitSpawnFn?: typeof gitSpawn, readHeadSourceFn?: typeof readHeadSource }} [deps]
|
|
277
|
+
* @param {{ reportFn?: Function, classifier?: Function, runOnPoolFn?: typeof runOnPool, headRef?: string|null, gitSpawnFn?: typeof gitSpawn, readHeadSourceFn?: typeof readHeadSource, serialThreshold?: number }} [deps]
|
|
272
278
|
* @returns {Promise<{ totalFiles: number, jsFiles: number, maintainability: object[], criticalFindings: Finding[], mediumFindings: Finding[] }>}
|
|
273
279
|
*/
|
|
274
280
|
export async function analyzeChangedFiles(
|
|
@@ -280,6 +286,7 @@ export async function analyzeChangedFiles(
|
|
|
280
286
|
headRef = null,
|
|
281
287
|
gitSpawnFn = gitSpawn,
|
|
282
288
|
readHeadSourceFn = readHeadSource,
|
|
289
|
+
serialThreshold = SERIAL_THRESHOLD,
|
|
283
290
|
} = {},
|
|
284
291
|
) {
|
|
285
292
|
const results = {
|
|
@@ -308,7 +315,7 @@ export async function analyzeChangedFiles(
|
|
|
308
315
|
const customReportFn = reportFn != null;
|
|
309
316
|
|
|
310
317
|
// Serial path: small batches, or whenever a caller injects its own scorer.
|
|
311
|
-
if (jsFiles.length <
|
|
318
|
+
if (jsFiles.length < serialThreshold || customReportFn) {
|
|
312
319
|
for (let i = 0; i < jsFiles.length; i += 1) {
|
|
313
320
|
const relPath = jsFiles[i];
|
|
314
321
|
const source = sources[i];
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*
|
|
29
29
|
* @type {Readonly<Record<ReviewDepth, string>>}
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
const DEPTH_DIRECTIVES = Object.freeze({
|
|
32
32
|
light:
|
|
33
33
|
'Review depth: LIGHT. Run a single pass focused on spec adherence over the ' +
|
|
34
34
|
'changed surface — confirm the change matches its stated intent. Reduce the ' +
|
|
@@ -54,7 +54,7 @@ export const DEPTH_DIRECTIVES = Object.freeze({
|
|
|
54
54
|
* @param {unknown} depth
|
|
55
55
|
* @returns {ReviewDepth}
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
function normalizeDepth(depth) {
|
|
58
58
|
return depth === 'light' || depth === 'deep' ? depth : 'standard';
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -59,7 +59,7 @@ export const SECURITY_REVIEW_REMEDIATIONS = Object.freeze({
|
|
|
59
59
|
* @param {{ spawnFn?: typeof spawnSync }} [opts]
|
|
60
60
|
* @returns {boolean}
|
|
61
61
|
*/
|
|
62
|
-
|
|
62
|
+
function defaultProbeClaudeCli(opts = {}) {
|
|
63
63
|
const spawnFn = opts.spawnFn ?? spawnSync;
|
|
64
64
|
try {
|
|
65
65
|
const result = spawnFn('claude', ['--version'], {
|
|
@@ -162,7 +162,7 @@ export function parseSecurityReviewFindings(rawStdout) {
|
|
|
162
162
|
* (Story #3937) so a high-risk Epic instructs the model toward a deeper
|
|
163
163
|
* second-pass review while a low-risk one keeps it light.
|
|
164
164
|
*/
|
|
165
|
-
|
|
165
|
+
const SECURITY_REVIEW_INVOKE_PROMPT =
|
|
166
166
|
'Run /security-review against the diff `{baseRef}`...`{headRef}` ' +
|
|
167
167
|
'for {scopeLabel} #{ticketId}. {depthDirective} After the review, emit ' +
|
|
168
168
|
'ONLY a JSON array of findings on stdout with this exact shape:\n\n' +
|
|
@@ -208,7 +208,7 @@ export function buildSecurityReviewPrompt(input) {
|
|
|
208
208
|
* @param {ReviewInput} input
|
|
209
209
|
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
210
210
|
*/
|
|
211
|
-
|
|
211
|
+
function defaultInvokeSecurityReview(input) {
|
|
212
212
|
const prompt = buildSecurityReviewPrompt(input);
|
|
213
213
|
const result = spawnSync('claude', ['--print', prompt], {
|
|
214
214
|
encoding: 'utf-8',
|
|
@@ -63,6 +63,12 @@
|
|
|
63
63
|
|
|
64
64
|
import { gh as defaultGh } from '../../../gh-exec.js';
|
|
65
65
|
import { resolveAutoMergeArmCwd } from '../../auto-merge-cwd.js';
|
|
66
|
+
import {
|
|
67
|
+
advisoryCheckFailedBlocksArm,
|
|
68
|
+
deriveRedHeadRuns,
|
|
69
|
+
formatAdvisoryGateReason,
|
|
70
|
+
selectBlockingRedRuns,
|
|
71
|
+
} from '../../merge-poll.js';
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
74
|
* Arm reasons that mean **the operator deliberately owns the merge** — the PR
|
|
@@ -321,6 +327,116 @@ function makeDefaultGhAutoMergeRunner(gh) {
|
|
|
321
327
|
};
|
|
322
328
|
}
|
|
323
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Evaluate the pre-arm advisory-gate verdict (Story #5096).
|
|
332
|
+
*
|
|
333
|
+
* GitHub native auto-merge is defined to wait on REQUIRED contexts only, so a
|
|
334
|
+
* red ADVISORY quality gate — a coverage, mutation, duplication, a11y or
|
|
335
|
+
* bundle-size ratchet a consumer deliberately left non-required — is merged
|
|
336
|
+
* straight past once the required contexts go green. The arming decision is
|
|
337
|
+
* mandrel's, not GitHub's, so this is where it has to be made.
|
|
338
|
+
*
|
|
339
|
+
* Reads the SAME head-anchored derivation the merge wait uses
|
|
340
|
+
* (`readPrWaitProbe` → `deriveRedHeadRuns`), so a `CANCELLED` superseded-push
|
|
341
|
+
* run or a sibling-invalidated run is never mistaken for a red gate (the
|
|
342
|
+
* #4695 / #4710 trap), and `mergeStateStatus: UNSTABLE` supplies the
|
|
343
|
+
* required-vs-advisory discrimination the rollup itself lacks.
|
|
344
|
+
*
|
|
345
|
+
* **Non-fatal and fail-open in every degraded case.** A probe error, an absent
|
|
346
|
+
* or `UNKNOWN` merge state, or a disabled knob all return "do not block" — the
|
|
347
|
+
* arm proceeds exactly as it did before this Story. Only a positive verdict
|
|
348
|
+
* refuses.
|
|
349
|
+
*
|
|
350
|
+
* @param {object} args
|
|
351
|
+
* @returns {Promise<{ blocked: boolean, blockingRuns?: Array<object>, reason?: string }>}
|
|
352
|
+
*/
|
|
353
|
+
async function readAdvisoryProbe({ prNumber, gh }) {
|
|
354
|
+
const view = await (gh ?? defaultGh).pr.view(prNumber, [
|
|
355
|
+
'mergeStateStatus',
|
|
356
|
+
'statusCheckRollup',
|
|
357
|
+
]);
|
|
358
|
+
return {
|
|
359
|
+
mergeStateStatus:
|
|
360
|
+
typeof view?.mergeStateStatus === 'string'
|
|
361
|
+
? view.mergeStateStatus
|
|
362
|
+
: undefined,
|
|
363
|
+
redHeadRuns: deriveRedHeadRuns(view?.statusCheckRollup),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Disarm GitHub native auto-merge on a PR (Story #5096).
|
|
369
|
+
*
|
|
370
|
+
* Lives here, beside the arm, because `lifecycle-lint`'s merge-lockout rule
|
|
371
|
+
* confines every `gh pr merge` invocation to this module: auto-merge
|
|
372
|
+
* enablement — and therefore its reversal — must flow through the Story close
|
|
373
|
+
* path rather than being spelled out wherever a caller happens to need it.
|
|
374
|
+
* The merge wait imports this rather than shelling out itself.
|
|
375
|
+
*
|
|
376
|
+
* Best-effort by contract: the caller blocks the Story either way, and a
|
|
377
|
+
* failed disarm is reported rather than thrown, because the one thing it
|
|
378
|
+
* cannot do is stop GitHub from landing the PR.
|
|
379
|
+
*
|
|
380
|
+
* @returns {Promise<boolean>} whether the disarm actually took.
|
|
381
|
+
*/
|
|
382
|
+
export async function disarmAutoMerge({ prNumber, gh, progress }) {
|
|
383
|
+
try {
|
|
384
|
+
await (gh ?? defaultGh).pr.merge(String(prNumber), ['--disable-auto']);
|
|
385
|
+
progress?.(
|
|
386
|
+
'CONFIRM',
|
|
387
|
+
`🔓 Auto-merge DISARMED on PR #${prNumber} — the PR stays open and hand-mergeable.`,
|
|
388
|
+
);
|
|
389
|
+
return true;
|
|
390
|
+
} catch (err) {
|
|
391
|
+
progress?.(
|
|
392
|
+
'CONFIRM',
|
|
393
|
+
`⚠️ Could not disarm auto-merge on PR #${prNumber} (${err?.message ?? err}) — ` +
|
|
394
|
+
'GitHub may still land it when the required checks pass. Disarm by hand.',
|
|
395
|
+
);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function evaluateAdvisoryGate({
|
|
401
|
+
prNumber,
|
|
402
|
+
gh,
|
|
403
|
+
blockOnAdvisoryFailure,
|
|
404
|
+
advisoryAllowlist,
|
|
405
|
+
readPrWaitProbeFn,
|
|
406
|
+
progress,
|
|
407
|
+
}) {
|
|
408
|
+
if (!blockOnAdvisoryFailure) return { blocked: false };
|
|
409
|
+
let probe;
|
|
410
|
+
try {
|
|
411
|
+
probe = await readPrWaitProbeFn({ prNumber, gh });
|
|
412
|
+
} catch (err) {
|
|
413
|
+
progress?.(
|
|
414
|
+
'PR',
|
|
415
|
+
`⚠️ Advisory-gate probe failed (${err?.message ?? err}) — arming anyway.`,
|
|
416
|
+
);
|
|
417
|
+
return { blocked: false };
|
|
418
|
+
}
|
|
419
|
+
if (probe?.error) {
|
|
420
|
+
progress?.(
|
|
421
|
+
'PR',
|
|
422
|
+
`⚠️ Advisory-gate probe unavailable (${probe.error}) — arming anyway.`,
|
|
423
|
+
);
|
|
424
|
+
return { blocked: false };
|
|
425
|
+
}
|
|
426
|
+
if (!advisoryCheckFailedBlocksArm(probe, advisoryAllowlist)) {
|
|
427
|
+
return { blocked: false };
|
|
428
|
+
}
|
|
429
|
+
const blockingRuns = selectBlockingRedRuns(
|
|
430
|
+
probe.redHeadRuns,
|
|
431
|
+
advisoryAllowlist,
|
|
432
|
+
);
|
|
433
|
+
return {
|
|
434
|
+
blocked: true,
|
|
435
|
+
blockingRuns,
|
|
436
|
+
reason: formatAdvisoryGateReason(blockingRuns),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
324
440
|
/**
|
|
325
441
|
* Dispatch auto-merge enablement based on `--no-auto-merge`, an
|
|
326
442
|
* unparseable PR number, or a `gh` failure. Returns the structured
|
|
@@ -347,8 +463,11 @@ export async function runAutoMergePhase({
|
|
|
347
463
|
prUrl,
|
|
348
464
|
noAutoMerge,
|
|
349
465
|
autoMergePolicy = 'trust-ci',
|
|
466
|
+
blockOnAdvisoryFailure = true,
|
|
467
|
+
advisoryAllowlist = [],
|
|
350
468
|
gh,
|
|
351
469
|
progress,
|
|
470
|
+
readPrWaitProbeFn = readAdvisoryProbe,
|
|
352
471
|
}) {
|
|
353
472
|
if (noAutoMerge) {
|
|
354
473
|
progress('PR', '⏭ Auto-merge disabled (--no-auto-merge).');
|
|
@@ -381,6 +500,32 @@ export async function runAutoMergePhase({
|
|
|
381
500
|
autoMergeReason: 'pr-number-unparseable',
|
|
382
501
|
};
|
|
383
502
|
}
|
|
503
|
+
// Story #5096 — refuse to arm over a genuinely red ADVISORY check. This
|
|
504
|
+
// gate covers the narrow case where the gate is ALREADY red at close time
|
|
505
|
+
// (a re-close, or a gate carried over from an earlier push); the merge
|
|
506
|
+
// wait owns the common case, where the gate reddens after arming.
|
|
507
|
+
const advisory = await evaluateAdvisoryGate({
|
|
508
|
+
prNumber,
|
|
509
|
+
gh,
|
|
510
|
+
blockOnAdvisoryFailure,
|
|
511
|
+
advisoryAllowlist,
|
|
512
|
+
readPrWaitProbeFn,
|
|
513
|
+
progress,
|
|
514
|
+
});
|
|
515
|
+
if (advisory.blocked) {
|
|
516
|
+
progress(
|
|
517
|
+
'PR',
|
|
518
|
+
`🛑 Auto-merge NOT armed on PR #${prNumber}: ${advisory.reason}`,
|
|
519
|
+
);
|
|
520
|
+
return {
|
|
521
|
+
autoMergeEnabled: false,
|
|
522
|
+
autoMergeReason: 'advisory-gate-red',
|
|
523
|
+
advisoryGate: {
|
|
524
|
+
blockingRuns: advisory.blockingRuns,
|
|
525
|
+
reason: advisory.reason,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
384
529
|
const result = await enableAutoMergeWith({ cwd, prNumber, gh });
|
|
385
530
|
if (result.enabled) {
|
|
386
531
|
if (result.directMerged) {
|