mandrel 2.53.0 → 2.55.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/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +4 -2
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +158 -7
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +68 -5
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +60 -114
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +19 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/findings/route-finding.js +4 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +241 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +24 -1
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +119 -6
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +59 -38
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +70 -6
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +40 -16
- package/.agents/workflows/helpers/deliver-story-reference.md +7 -4
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +8 -1
- package/.agents/workflows/mandrel-plan.md +4 -7
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +34 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -45,13 +45,20 @@
|
|
|
45
45
|
|
|
46
46
|
import fs from 'node:fs';
|
|
47
47
|
import path from 'node:path';
|
|
48
|
-
|
|
49
48
|
import { assertEnvelope } from './lib/baselines/envelope.js';
|
|
50
49
|
import {
|
|
50
|
+
baselineRegenerateRemedy,
|
|
51
51
|
kindFromEnvelope,
|
|
52
52
|
mergeEnvelopes,
|
|
53
|
+
mergePlainBaseline,
|
|
54
|
+
plainKindFromEnvelope,
|
|
55
|
+
renderStampConflict,
|
|
53
56
|
} from './lib/baselines/merge-envelopes.js';
|
|
54
57
|
import { writeFile as writeEnvelopeFile } from './lib/baselines/writer.js';
|
|
58
|
+
import {
|
|
59
|
+
BASELINE_MERGE_DRIVER_REMEDY,
|
|
60
|
+
ensureBaselineMergeDriver,
|
|
61
|
+
} from './lib/bootstrap/baseline-merge-driver.js';
|
|
55
62
|
import { spawnChild } from './lib/child-exec.js';
|
|
56
63
|
import { runAsCli } from './lib/cli-utils.js';
|
|
57
64
|
|
|
@@ -144,11 +151,86 @@ function delegateToGit(basePath, oursPath, theirsPath) {
|
|
|
144
151
|
return result.status ?? 1;
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Which merge does this file get?
|
|
156
|
+
*
|
|
157
|
+
* Two families answer, and neither is "all of `baselines/*.json`":
|
|
158
|
+
*
|
|
159
|
+
* - **Envelope kinds** — the eight kernel kinds, merged by the kind
|
|
160
|
+
* module's own `rowIdentity` with the rollup RE-DERIVED.
|
|
161
|
+
* - **Plain row baselines** — `cyclomatic`, `dead-exports` and
|
|
162
|
+
* `dead-exports-production`, which are row sets with a real identity but
|
|
163
|
+
* no kernel protocol. Until Story #5277 these fell through to
|
|
164
|
+
* `git merge-file` even though `.gitattributes` routes them here, so the
|
|
165
|
+
* attribute promised a row merge the driver never performed.
|
|
166
|
+
*
|
|
167
|
+
* Anything else — arch-cycles, audit-ledger, context-budget,
|
|
168
|
+
* workflow-citations — resolves `null` and is handed back to git unchanged.
|
|
169
|
+
*
|
|
170
|
+
* @param {unknown} ours
|
|
171
|
+
* @param {unknown} theirs
|
|
172
|
+
* @returns {{ kind: string, envelopeKind: boolean }|null}
|
|
173
|
+
*/
|
|
174
|
+
function resolveMergeTarget(ours, theirs) {
|
|
175
|
+
const envelopeKind = kindFromEnvelope(ours) ?? kindFromEnvelope(theirs);
|
|
176
|
+
if (envelopeKind) return { kind: envelopeKind, envelopeKind: true };
|
|
177
|
+
const plain = plainKindFromEnvelope(ours) ?? plainKindFromEnvelope(theirs);
|
|
178
|
+
if (plain) return { kind: plain, envelopeKind: false };
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Serialize the merged result into `%A`. Envelope kinds go through the shared
|
|
184
|
+
* writer (which validates against the per-kind schema); plain baselines are
|
|
185
|
+
* written with the same `JSON.stringify(…, null, 2)` + trailing newline their
|
|
186
|
+
* own generators use, which is what keeps a clean merge byte-identical to a
|
|
187
|
+
* regeneration.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} oursPath
|
|
190
|
+
* @param {{ envelope: object }} merged
|
|
191
|
+
* @param {boolean} isEnvelopeKind
|
|
192
|
+
*/
|
|
193
|
+
function writeMerged(oursPath, merged, isEnvelopeKind) {
|
|
194
|
+
if (isEnvelopeKind) {
|
|
195
|
+
writeEnvelopeFile(oursPath, merged.envelope);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
fs.writeFileSync(oursPath, `${JSON.stringify(merged.envelope, null, 2)}\n`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Register the driver in this clone (`--install`).
|
|
203
|
+
*
|
|
204
|
+
* The driver script installs its own registration because the registration is
|
|
205
|
+
* the half that cannot travel: `.gitattributes` is tracked and ships with the
|
|
206
|
+
* repo, `merge.mandrel-baseline.driver` is per-clone git config and is absent
|
|
207
|
+
* in every fresh clone — silently, with git falling back to a text merge and
|
|
208
|
+
* reporting nothing. Wired into this repo's `prepare` script so `npm install`
|
|
209
|
+
* completes the registration, and idempotent so every later `prepare` is a
|
|
210
|
+
* no-op.
|
|
211
|
+
*
|
|
212
|
+
* @returns {number} Process exit code.
|
|
213
|
+
*/
|
|
214
|
+
function runInstall() {
|
|
215
|
+
const result = ensureBaselineMergeDriver({ projectRoot: process.cwd() });
|
|
216
|
+
if (result.config === 'failed') {
|
|
217
|
+
process.stderr.write(
|
|
218
|
+
`merge-baseline: could not register the merge driver.\n → ${BASELINE_MERGE_DRIVER_REMEDY}\n`,
|
|
219
|
+
);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write(
|
|
223
|
+
`merge-baseline: driver ${result.action} (attributes=${result.attributes}, config=${result.config}) → ${result.command}\n`,
|
|
224
|
+
);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
147
228
|
/**
|
|
148
229
|
* @param {string[]} argv Positional arguments: %O %A %B [%P].
|
|
149
230
|
* @returns {number} Process exit code.
|
|
150
231
|
*/
|
|
151
232
|
export function runMergeBaseline(argv) {
|
|
233
|
+
if (argv.includes('--install')) return runInstall();
|
|
152
234
|
const [baseArg, oursArg, theirsArg, mergedPath] = argv;
|
|
153
235
|
if (!baseArg || !oursArg || !theirsArg) {
|
|
154
236
|
process.stderr.write(
|
|
@@ -168,37 +250,106 @@ export function runMergeBaseline(argv) {
|
|
|
168
250
|
|
|
169
251
|
const ours = readSide(oursPath);
|
|
170
252
|
const theirs = readSide(theirsPath);
|
|
171
|
-
const
|
|
253
|
+
const target =
|
|
254
|
+
ours === undefined || theirs === undefined
|
|
255
|
+
? null
|
|
256
|
+
: resolveMergeTarget(ours, theirs);
|
|
257
|
+
if (!target) return delegateToGit(basePath, oursPath, theirsPath);
|
|
172
258
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
259
|
+
return mergeResolved({
|
|
260
|
+
target,
|
|
261
|
+
base: readSide(basePath),
|
|
262
|
+
ours,
|
|
263
|
+
theirs,
|
|
264
|
+
basePath,
|
|
265
|
+
oursPath,
|
|
266
|
+
theirsPath,
|
|
267
|
+
label: mergedPath || oursPath,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
177
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Merge a file whose kind the driver DOES understand, and leave the result in
|
|
273
|
+
* `%A`. A merge that throws is handed back to git rather than half-written:
|
|
274
|
+
* the driver must never invent a result for a baseline it could not model.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} ctx
|
|
277
|
+
* @returns {number} Process exit code.
|
|
278
|
+
*/
|
|
279
|
+
function mergeResolved({
|
|
280
|
+
target,
|
|
281
|
+
base,
|
|
282
|
+
ours,
|
|
283
|
+
theirs,
|
|
284
|
+
basePath,
|
|
285
|
+
oursPath,
|
|
286
|
+
theirsPath,
|
|
287
|
+
label,
|
|
288
|
+
}) {
|
|
289
|
+
const { kind, envelopeKind: isEnvelopeKind } = target;
|
|
178
290
|
let merged;
|
|
179
291
|
try {
|
|
180
|
-
merged =
|
|
292
|
+
merged = isEnvelopeKind
|
|
293
|
+
? mergeEnvelopes({ base, ours, theirs, kind })
|
|
294
|
+
: mergePlainBaseline({ base, ours, theirs, kind });
|
|
181
295
|
} catch (err) {
|
|
182
296
|
process.stderr.write(`merge-baseline: ${kind}: ${err.message}\n`);
|
|
183
297
|
return delegateToGit(basePath, oursPath, theirsPath);
|
|
184
298
|
}
|
|
185
299
|
|
|
186
|
-
const rowConflicts = merged.conflicts.filter((c) => c.scope === 'row');
|
|
187
|
-
const envelopeConflicts = merged.conflicts.filter(
|
|
188
|
-
(c) => c.scope === 'envelope',
|
|
189
|
-
);
|
|
190
|
-
|
|
191
300
|
// Write the canonical projection first even when conflicted: the marker
|
|
192
301
|
// rendering operates on exactly the bytes a clean merge would have left,
|
|
193
302
|
// so the merged remainder of a conflicted file is identical to it.
|
|
194
|
-
|
|
303
|
+
writeMerged(oursPath, merged, isEnvelopeKind);
|
|
195
304
|
|
|
196
305
|
if (merged.conflicts.length === 0) {
|
|
197
|
-
assertEnvelope(merged.envelope);
|
|
306
|
+
if (isEnvelopeKind) assertEnvelope(merged.envelope);
|
|
198
307
|
return 0;
|
|
199
308
|
}
|
|
309
|
+
return markConflicts({ kind, merged, oursPath, label });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Report a conflicted merge and render its markers into `%A`.
|
|
314
|
+
*
|
|
315
|
+
* Both scopes get markers. An envelope-level conflict used to be reported on
|
|
316
|
+
* stderr alone, leaving a file that looked cleanly merged while git held the
|
|
317
|
+
* path unmerged — the operator had to reconstruct from scrollback which stamp
|
|
318
|
+
* disagreed.
|
|
319
|
+
*
|
|
320
|
+
* @param {{ kind: string, merged: object, oursPath: string, label: string }} ctx
|
|
321
|
+
* @returns {number} Always 1 — a conflicted merge.
|
|
322
|
+
*/
|
|
323
|
+
function markConflicts({ kind, merged, oursPath, label }) {
|
|
324
|
+
const rowConflicts = merged.conflicts.filter((c) => c.scope === 'row');
|
|
325
|
+
const envelopeConflicts = merged.conflicts.filter(
|
|
326
|
+
(c) => c.scope === 'envelope',
|
|
327
|
+
);
|
|
328
|
+
reportConflicts({ kind, label, rowConflicts, envelopeConflicts });
|
|
329
|
+
|
|
330
|
+
let text = fs.readFileSync(oursPath, 'utf8');
|
|
331
|
+
if (envelopeConflicts.length > 0) {
|
|
332
|
+
text = renderStampConflict(text, envelopeConflicts);
|
|
333
|
+
}
|
|
334
|
+
if (rowConflicts.length > 0) {
|
|
335
|
+
text = renderConflictMarkers(text, rowConflicts);
|
|
336
|
+
}
|
|
337
|
+
fs.writeFileSync(oursPath, text);
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
200
340
|
|
|
201
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Report every conflict on stderr, then name the regeneration command.
|
|
343
|
+
*
|
|
344
|
+
* The regenerate line is not decoration. The merged file's rollup was derived
|
|
345
|
+
* from a row set that still carries conflict markers, so resolving the markers
|
|
346
|
+
* by hand leaves the rollup describing a tree nobody scored — the same silent
|
|
347
|
+
* wrong number the driver exists to prevent, arrived at from the other
|
|
348
|
+
* direction. Saying it here is the only place an operator sees it.
|
|
349
|
+
*
|
|
350
|
+
* @param {{ kind: string, label: string, rowConflicts: Array<object>, envelopeConflicts: Array<object> }} args
|
|
351
|
+
*/
|
|
352
|
+
function reportConflicts({ kind, label, rowConflicts, envelopeConflicts }) {
|
|
202
353
|
for (const conflict of envelopeConflicts) {
|
|
203
354
|
process.stderr.write(
|
|
204
355
|
`merge-baseline: conflict ${kind} envelope key "${conflict.identity}" in ${label} — ours ${JSON.stringify(conflict.ours)}, theirs ${JSON.stringify(conflict.theirs)}\n`,
|
|
@@ -209,12 +360,11 @@ export function runMergeBaseline(argv) {
|
|
|
209
360
|
`merge-baseline: conflict ${kind} row "${conflict.identity}" in ${label}\n`,
|
|
210
361
|
);
|
|
211
362
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return 1;
|
|
363
|
+
process.stderr.write(
|
|
364
|
+
`merge-baseline: ${label} is conflicted — the rollup in it was derived from ` +
|
|
365
|
+
`unresolved rows and must not be trusted. After resolving the markers, ` +
|
|
366
|
+
`regenerate it: ${baselineRegenerateRemedy(kind)}\n`,
|
|
367
|
+
);
|
|
218
368
|
}
|
|
219
369
|
|
|
220
370
|
function main() {
|
|
@@ -233,6 +383,10 @@ runAsCli(import.meta.url, main, {
|
|
|
233
383
|
['%A', 'Our version — the driver writes its result here.'],
|
|
234
384
|
['%B', 'Their version.'],
|
|
235
385
|
['%P', 'Real pathname being merged; used in conflict messages.'],
|
|
386
|
+
[
|
|
387
|
+
'--install',
|
|
388
|
+
'Register the driver in this clone (.gitattributes line + the per-clone merge.mandrel-baseline.driver config) and exit. Idempotent.',
|
|
389
|
+
],
|
|
236
390
|
],
|
|
237
391
|
},
|
|
238
392
|
});
|
|
@@ -15,13 +15,34 @@
|
|
|
15
15
|
* Extracted from `../github.js` in Story #1846 / Task #1857.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Needles that mean "this API surface does not exist here", not "this call
|
|
20
|
+
* failed". Every one names a *schema* fact — an absent GraphQL field, a
|
|
21
|
+
* disabled feature — so a match is a settled answer no retry can improve.
|
|
22
|
+
*
|
|
23
|
+
* The bare `sub-issues` needle used to sit in this list and had to go: it
|
|
24
|
+
* matches the endpoint's own name, so any error that merely *mentions* the
|
|
25
|
+
* surface classified as `feature-disabled`. A secondary rate limit delivered
|
|
26
|
+
* as `HTTP 403: API rate limit exceeded while fetching sub-issues` is the live
|
|
27
|
+
* case — genuinely transient, but bucketed here it bypassed
|
|
28
|
+
* `withTransientRetry` entirely and the Epic rollup read the burst as "this
|
|
29
|
+
* repo has no sub-issues", degrading to the body checklist on a read that
|
|
30
|
+
* would have succeeded a second later. The narrower `subissues` spellings
|
|
31
|
+
* below still catch the real schema errors, because GraphQL names the field
|
|
32
|
+
* without the hyphen.
|
|
33
|
+
*/
|
|
18
34
|
const FEATURE_DISABLED_MESSAGES = [
|
|
19
35
|
'feature not available',
|
|
20
36
|
'feature is not enabled',
|
|
21
37
|
"field 'subissues'",
|
|
22
38
|
'field "subissues"',
|
|
23
39
|
'subissues is not available',
|
|
24
|
-
|
|
40
|
+
// The field, never the endpoint. `<x> field` cannot appear in "while
|
|
41
|
+
// fetching sub-issues", which is what makes these safe to keep after the
|
|
42
|
+
// bare needle's removal.
|
|
43
|
+
'subissues field',
|
|
44
|
+
'sub_issues field',
|
|
45
|
+
'sub-issues field',
|
|
25
46
|
"doesn't exist on type",
|
|
26
47
|
'does not exist on type',
|
|
27
48
|
'unknown field',
|
|
@@ -20,7 +20,7 @@ import { Logger } from '../../lib/Logger.js';
|
|
|
20
20
|
import { concurrentMap } from '../../lib/util/concurrent-map.js';
|
|
21
21
|
import { isNotFoundError } from './branch-protection.js';
|
|
22
22
|
import { classifyGithubError, withTransientRetry } from './errors.js';
|
|
23
|
-
import { issueToEpic } from './mappers.js';
|
|
23
|
+
import { issueToEpic, issueToTicket, subIssueNodeToTicket } from './mappers.js';
|
|
24
24
|
import {
|
|
25
25
|
defaultRetryWarn,
|
|
26
26
|
paginateRest,
|
|
@@ -54,6 +54,32 @@ function classifySearchRetry(err) {
|
|
|
54
54
|
*/
|
|
55
55
|
export const SUBTICKET_HYDRATION_CONCURRENCY = 8;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The `Issue.parent` read backing {@link IssuesGateway#getParentIssue}.
|
|
59
|
+
*
|
|
60
|
+
* Node selection is deliberately identical to `SUB_ISSUES_QUERY`'s, so the
|
|
61
|
+
* parent and the children a caller holds come back in one shape and
|
|
62
|
+
* `subIssueNodeToTicket` maps both. Addressed by `owner/repo/number` rather
|
|
63
|
+
* than by node id because every caller starts from an issue number and would
|
|
64
|
+
* otherwise pay a round-trip just to learn the node id.
|
|
65
|
+
*/
|
|
66
|
+
const PARENT_ISSUE_QUERY = `query($owner: String!, $repo: String!, $number: Int!) {
|
|
67
|
+
repository(owner: $owner, name: $repo) {
|
|
68
|
+
issue(number: $number) {
|
|
69
|
+
parent {
|
|
70
|
+
number
|
|
71
|
+
databaseId
|
|
72
|
+
id
|
|
73
|
+
title
|
|
74
|
+
body
|
|
75
|
+
state
|
|
76
|
+
labels(first: 30) { nodes { name } }
|
|
77
|
+
assignees(first: 20) { nodes { login } }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}`;
|
|
82
|
+
|
|
57
83
|
// Re-export so existing test consumers that previously imported
|
|
58
84
|
// `paginateRest` from this module continue to work without an extra
|
|
59
85
|
// migration step.
|
|
@@ -124,6 +150,85 @@ export class IssuesGateway {
|
|
|
124
150
|
return issues.filter((issue) => !issue?.pull_request);
|
|
125
151
|
}
|
|
126
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The same scan as {@link listIssuesByLabel}, mapped through
|
|
155
|
+
* `issueToTicket` — one **declared** shape instead of a raw REST payload.
|
|
156
|
+
*
|
|
157
|
+
* The two differ in exactly the field that keeps biting: the REST payload
|
|
158
|
+
* calls the issue number `number` and the database id `id`, while every
|
|
159
|
+
* mapped read calls the issue number `id`. Consumers that could be handed
|
|
160
|
+
* either wrote `number ?? id` to cope, and that fallback is not a
|
|
161
|
+
* defensive nicety — it is a live bug, because on a *mapped* ticket `id`
|
|
162
|
+
* is the number and on a *raw* one it is the database id. A caller that
|
|
163
|
+
* ever receives the raw shape silently addresses issues by database id.
|
|
164
|
+
*
|
|
165
|
+
* `url` is carried alongside the mapped fields because two consumers
|
|
166
|
+
* (`epic-candidates`, `dependency-candidates`) render a link and
|
|
167
|
+
* `issueToTicket` drops `html_url`. It is the only addition; everything
|
|
168
|
+
* else is exactly what every other single-issue read returns.
|
|
169
|
+
*
|
|
170
|
+
* @param {{ state?: 'open'|'closed'|'all', labels?: string }} [opts]
|
|
171
|
+
* @returns {Promise<Array<object>>} Mapped tickets (`id` is the issue number).
|
|
172
|
+
* @field-manifest /repos/{owner}/{repo}/issues: number, id, node_id, title,
|
|
173
|
+
* body, labels, state, state_reason, assignees, html_url,
|
|
174
|
+
* pull_request
|
|
175
|
+
*/
|
|
176
|
+
async listTicketsByLabel(opts = {}) {
|
|
177
|
+
const issues = await this.listIssuesByLabel(opts);
|
|
178
|
+
return issues.map((issue) => ({
|
|
179
|
+
...issueToTicket(issue),
|
|
180
|
+
url: issue.html_url ?? null,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resolve an issue's container parent in **one** request.
|
|
186
|
+
*
|
|
187
|
+
* The rollup's child→parent lookup used to scan every open `type::epic`
|
|
188
|
+
* issue and read each one's children looking for the Story it was handed:
|
|
189
|
+
* O(open Epics) requests to answer a question the API answers directly.
|
|
190
|
+
* `Issue.parent` is the native sub-issue edge read backwards, so a Story
|
|
191
|
+
* with a container costs one call and a Story without one costs the same.
|
|
192
|
+
*
|
|
193
|
+
* Returns `null` — never throws — when the issue has no parent, when the
|
|
194
|
+
* response is shaped unexpectedly, or when the sub-issues feature is
|
|
195
|
+
* unavailable on this repo. A null is "no parent resolved here", which is
|
|
196
|
+
* exactly what the caller's body-checklist fallback exists for; turning a
|
|
197
|
+
* disabled feature into an exception would convert a degraded lookup into a
|
|
198
|
+
* failed lifecycle edge.
|
|
199
|
+
*
|
|
200
|
+
* @param {number} number Issue number whose parent to resolve.
|
|
201
|
+
* @returns {Promise<object|null>} Mapped parent ticket, or null.
|
|
202
|
+
* @field-manifest GraphQL Issue.parent: number, id, title, body, state,
|
|
203
|
+
* labels.nodes.name, assignees.nodes.login
|
|
204
|
+
*/
|
|
205
|
+
async getParentIssue(number) {
|
|
206
|
+
const issueNumber = Number(number);
|
|
207
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0) return null;
|
|
208
|
+
let data;
|
|
209
|
+
try {
|
|
210
|
+
data = await withTransientRetry(
|
|
211
|
+
() =>
|
|
212
|
+
this.ghGraphql(
|
|
213
|
+
PARENT_ISSUE_QUERY,
|
|
214
|
+
{ owner: this.owner, repo: this.repo, number: issueNumber },
|
|
215
|
+
{ headers: { 'GraphQL-Features': 'sub_issues' } },
|
|
216
|
+
),
|
|
217
|
+
{
|
|
218
|
+
label: `getParentIssue #${issueNumber}`,
|
|
219
|
+
onRetry: defaultRetryWarn,
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
Logger.warn(
|
|
224
|
+
`[GitHubProvider] parent lookup for #${issueNumber} degraded to none ` +
|
|
225
|
+
`(${err?.message ?? err}).`,
|
|
226
|
+
);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return subIssueNodeToTicket(data?.repository?.issue?.parent ?? null);
|
|
230
|
+
}
|
|
231
|
+
|
|
127
232
|
/**
|
|
128
233
|
* Search issues by a free-text query via the REST search API
|
|
129
234
|
* (`GET /search/issues`). Deliberately REST, **not** GraphQL: transient
|
|
@@ -150,18 +150,28 @@ export async function addSubIssueEdges({
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Link child Stories to a container Epic, resolving each child's **database
|
|
153
|
-
* id** from its issue number
|
|
153
|
+
* id** from its issue number.
|
|
154
154
|
*
|
|
155
155
|
* Callers hold issue numbers (that is what `plan-persist` creates and what
|
|
156
156
|
* an operator types); the API wants database ids. Doing the translation here
|
|
157
157
|
* keeps that trap in one place instead of at every call site.
|
|
158
158
|
*
|
|
159
|
+
* `knownInternalIds` short-circuits the translation for children whose
|
|
160
|
+
* database id the caller already has. `createIssue` returns `internalId` in
|
|
161
|
+
* its response, so a cohort this run just created needs no lookup at all —
|
|
162
|
+
* the `getTicket` fan-out that used to run over every child was re-reading
|
|
163
|
+
* issues the same process had created seconds earlier, one round-trip per
|
|
164
|
+
* Story, to recover a field it had already been handed and thrown away.
|
|
165
|
+
* `getTicket` stays for the children a *resumed* run adopted, whose ids came
|
|
166
|
+
* from a listing rather than a create.
|
|
167
|
+
*
|
|
159
168
|
* Never throws: a child whose id cannot be resolved is counted as failed and
|
|
160
169
|
* the remaining edges still go out.
|
|
161
170
|
*
|
|
162
171
|
* @param {{
|
|
163
172
|
* epicNumber: number,
|
|
164
173
|
* childIssueNumbers: number[],
|
|
174
|
+
* knownInternalIds?: Map<number, number>|null,
|
|
165
175
|
* getTicket: (issueNumber: number) => Promise<{ internalId: number }>,
|
|
166
176
|
* owner: string,
|
|
167
177
|
* repo: string,
|
|
@@ -173,6 +183,7 @@ export async function addSubIssueEdges({
|
|
|
173
183
|
export async function linkStoriesToEpic({
|
|
174
184
|
epicNumber,
|
|
175
185
|
childIssueNumbers,
|
|
186
|
+
knownInternalIds = null,
|
|
176
187
|
getTicket,
|
|
177
188
|
owner,
|
|
178
189
|
repo,
|
|
@@ -182,10 +193,16 @@ export async function linkStoriesToEpic({
|
|
|
182
193
|
const numbers = Array.isArray(childIssueNumbers) ? childIssueNumbers : [];
|
|
183
194
|
if (numbers.length === 0) return { added: 0, skipped: 0, failed: 0 };
|
|
184
195
|
|
|
196
|
+
const known = knownInternalIds instanceof Map ? knownInternalIds : new Map();
|
|
185
197
|
let failed = 0;
|
|
186
198
|
const childInternalIds = [];
|
|
187
199
|
|
|
188
200
|
for (const childNumber of numbers) {
|
|
201
|
+
const alreadyKnown = known.get(Number(childNumber));
|
|
202
|
+
if (typeof alreadyKnown === 'number') {
|
|
203
|
+
childInternalIds.push(alreadyKnown);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
189
206
|
try {
|
|
190
207
|
const ticket = await getTicket(childNumber);
|
|
191
208
|
const internalId = ticket?.internalId;
|
|
@@ -116,6 +116,8 @@ const DELEGATIONS = [
|
|
|
116
116
|
['graphql', 'issues.ghGraphql'],
|
|
117
117
|
['searchIssues', 'issues.searchIssues'],
|
|
118
118
|
['listIssuesByLabel', 'issues.listIssuesByLabel'],
|
|
119
|
+
['listTicketsByLabel', 'issues.listTicketsByLabel'],
|
|
120
|
+
['getParentIssue', 'issues.getParentIssue'],
|
|
119
121
|
['getEpic', 'issues.getEpic'],
|
|
120
122
|
['branchExists', 'issues.branchExists'],
|
|
121
123
|
['getSubTickets', 'issues.getSubTickets'],
|
|
@@ -126,6 +128,10 @@ const DELEGATIONS = [
|
|
|
126
128
|
['createIssue', 'tickets.createIssue'],
|
|
127
129
|
['updateTicket', 'tickets.updateTicket'],
|
|
128
130
|
['_applyLabelMutations', 'tickets._applyLabelMutations'],
|
|
131
|
+
['getNativeSubIssues', 'subIssues.getNativeSubIssues'],
|
|
132
|
+
// The private alias predates the declared `getNativeSubIssues` port above
|
|
133
|
+
// and still has call sites; both forward to the same gateway method, so the
|
|
134
|
+
// two can never answer differently while the older name is retired.
|
|
129
135
|
['_getNativeSubIssues', 'subIssues.getNativeSubIssues'],
|
|
130
136
|
['getTicketComments', 'comments.getTicketComments'],
|
|
131
137
|
['deleteComment', 'comments.deleteComment'],
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* node .agents/scripts/resolve-stories.js --ids 101-104 # inclusive range
|
|
31
31
|
* node .agents/scripts/resolve-stories.js --ids 101,102 --pretty
|
|
32
32
|
* node .agents/scripts/resolve-stories.js --ids 101 --no-native # skip the dependencies API
|
|
33
|
+
* node .agents/scripts/resolve-stories.js --ids 101 --allow-unlabelled
|
|
33
34
|
*
|
|
34
35
|
* Exit codes: 0 ok, 1 usage/resolution error.
|
|
35
36
|
*/
|
|
@@ -39,7 +40,7 @@ import { parseArgs } from 'node:util';
|
|
|
39
40
|
import { runAsCli } from './lib/cli-utils.js';
|
|
40
41
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
41
42
|
import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
|
|
42
|
-
import {
|
|
43
|
+
import { nativeChildReader } from './lib/orchestration/epic-container.js';
|
|
43
44
|
import { expandEpicIds } from './lib/orchestration/epic-expansion.js';
|
|
44
45
|
import {
|
|
45
46
|
buildStoriesEnvelope,
|
|
@@ -52,7 +53,18 @@ import { createProvider } from './lib/provider-factory.js';
|
|
|
52
53
|
import { concurrentMap } from './lib/util/concurrent-map.js';
|
|
53
54
|
import { paginateRest } from './providers/github/request-helpers.js';
|
|
54
55
|
|
|
55
|
-
export {
|
|
56
|
+
export {
|
|
57
|
+
buildStoriesEnvelope,
|
|
58
|
+
// Re-exported, not redefined (Story #5280). The reader now lives in
|
|
59
|
+
// `epic-container.js` beside the shape it reads, so the expansion path and
|
|
60
|
+
// the rollup share one definition — the pair whose divergence makes an Epic
|
|
61
|
+
// expandable but unclosable. The name stays exported here because two test
|
|
62
|
+
// modules import it from this entrypoint.
|
|
63
|
+
nativeChildReader,
|
|
64
|
+
parseIds,
|
|
65
|
+
readNativeBlockedBy,
|
|
66
|
+
toStoryRecord,
|
|
67
|
+
};
|
|
56
68
|
|
|
57
69
|
/**
|
|
58
70
|
* Bounded concurrency for the per-issue round-trips. Matches the edge-writer's
|
|
@@ -78,6 +90,11 @@ Options:
|
|
|
78
90
|
be mixed with Story ids.
|
|
79
91
|
--pretty Pretty-print the JSON envelope.
|
|
80
92
|
--no-native Skip the native blocked_by read (body edges only).
|
|
93
|
+
--allow-unlabelled
|
|
94
|
+
Resolve a Story carrying no agent::* label. Without it such a
|
|
95
|
+
Story is refused: the audit sweep files Stories without one on
|
|
96
|
+
purpose, and delivering one means dispatching a worker at
|
|
97
|
+
unenriched audit prose. Route it through /mandrel-plan first.
|
|
81
98
|
--help Show this help.
|
|
82
99
|
`;
|
|
83
100
|
|
|
@@ -93,29 +110,6 @@ export function resolveStoriesProvider({
|
|
|
93
110
|
return { provider: createProviderFn(config), config };
|
|
94
111
|
}
|
|
95
112
|
|
|
96
|
-
/**
|
|
97
|
-
* Read an Epic's native sub-issue children as issue numbers.
|
|
98
|
-
*
|
|
99
|
-
* Injected into `expandEpicIds` so the lib layer stays provider-agnostic,
|
|
100
|
-
* exactly as `paginate` is injected into `readNativeBlockedBy`. A provider
|
|
101
|
-
* without the GraphQL surface yields `[]`, and the Epic body's checklist
|
|
102
|
-
* carries the children on its own — as does an Epic carrying no resolvable
|
|
103
|
-
* node id, which `resolveEpicNodeId` reports rather than letting an
|
|
104
|
-
* `undefined` reach the API as a rejected `ID!` variable.
|
|
105
|
-
*
|
|
106
|
-
* @param {object} provider
|
|
107
|
-
* @returns {(epic: object) => Promise<number[]>}
|
|
108
|
-
*/
|
|
109
|
-
export function nativeChildReader(provider) {
|
|
110
|
-
return async (epic) => {
|
|
111
|
-
const nodeId = resolveEpicNodeId(epic);
|
|
112
|
-
if (nodeId === null) return [];
|
|
113
|
-
return (
|
|
114
|
-
provider?._getNativeSubIssues?.(nodeId, epic?.number ?? epic?.id) ?? []
|
|
115
|
-
);
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
113
|
/**
|
|
120
114
|
* Fetch every requested id and map it to a Story record, failing on the first
|
|
121
115
|
* id that is not a deliverable Story.
|
|
@@ -127,9 +121,10 @@ export function nativeChildReader(provider) {
|
|
|
127
121
|
*
|
|
128
122
|
* @param {object} provider
|
|
129
123
|
* @param {number[]} ids
|
|
124
|
+
* @param {{ allowUnlabelled?: boolean }} [options]
|
|
130
125
|
* @returns {Promise<object[]>}
|
|
131
126
|
*/
|
|
132
|
-
export async function fetchStories(provider, ids) {
|
|
127
|
+
export async function fetchStories(provider, ids, { allowUnlabelled } = {}) {
|
|
133
128
|
const { ids: resolvedIds, expansions } = await expandEpicIds({
|
|
134
129
|
ids,
|
|
135
130
|
getTicket: (id) => provider.getTicket(id),
|
|
@@ -151,7 +146,7 @@ export async function fetchStories(provider, ids) {
|
|
|
151
146
|
if (!issue) {
|
|
152
147
|
throw new Error(`[resolve-stories] Issue #${id} was not found.`);
|
|
153
148
|
}
|
|
154
|
-
return toStoryRecord(issue, id);
|
|
149
|
+
return toStoryRecord(issue, id, { allowUnlabelled });
|
|
155
150
|
},
|
|
156
151
|
{ concurrency: FETCH_CONCURRENCY },
|
|
157
152
|
);
|
|
@@ -237,19 +232,20 @@ export async function resolveForeignDone({ provider, dag, inSetIds }) {
|
|
|
237
232
|
* stdout are injected so the whole path is unit-testable without a live
|
|
238
233
|
* GitHub round-trip. Exported for testing.
|
|
239
234
|
*
|
|
240
|
-
* @param {{ ids: string, native?: boolean, pretty?: boolean
|
|
235
|
+
* @param {{ ids: string, native?: boolean, pretty?: boolean,
|
|
236
|
+
* allowUnlabelled?: boolean }} args
|
|
241
237
|
* @param {{ provider: object, config: object, stdout?: { write(s: string): void } }} deps
|
|
242
238
|
* @returns {Promise<number>}
|
|
243
239
|
*/
|
|
244
240
|
export async function runResolveStories(
|
|
245
|
-
{ ids: rawIds, native = true, pretty = false },
|
|
241
|
+
{ ids: rawIds, native = true, pretty = false, allowUnlabelled = false },
|
|
246
242
|
{ provider, config, stdout = process.stdout },
|
|
247
243
|
) {
|
|
248
244
|
const ids = parseIds(rawIds);
|
|
249
245
|
const owner = config.github?.owner;
|
|
250
246
|
const repo = config.github?.repo;
|
|
251
247
|
|
|
252
|
-
const stories = await fetchStories(provider, ids);
|
|
248
|
+
const stories = await fetchStories(provider, ids, { allowUnlabelled });
|
|
253
249
|
const nativeEdges = native
|
|
254
250
|
? await readNativeEdges({ provider, stories, owner, repo })
|
|
255
251
|
: new Map();
|
|
@@ -280,12 +276,29 @@ export async function runResolveStories(
|
|
|
280
276
|
return 0;
|
|
281
277
|
}
|
|
282
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Project the parsed flags onto the flow core's options object, so `main` reads
|
|
281
|
+
* as parse → help → run and the flag-name spellings live in one place.
|
|
282
|
+
*
|
|
283
|
+
* @param {Record<string, unknown>} values
|
|
284
|
+
* @returns {{ ids: string, native: boolean, pretty: boolean, allowUnlabelled: boolean }}
|
|
285
|
+
*/
|
|
286
|
+
function toRunOptions(values) {
|
|
287
|
+
return {
|
|
288
|
+
ids: values.ids,
|
|
289
|
+
native: values.native,
|
|
290
|
+
pretty: values.pretty,
|
|
291
|
+
allowUnlabelled: values['allow-unlabelled'],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
283
295
|
async function main() {
|
|
284
296
|
const { values } = parseArgs({
|
|
285
297
|
options: {
|
|
286
298
|
ids: { type: 'string' },
|
|
287
299
|
pretty: { type: 'boolean', default: false },
|
|
288
300
|
native: { type: 'boolean', default: true },
|
|
301
|
+
'allow-unlabelled': { type: 'boolean', default: false },
|
|
289
302
|
help: { type: 'boolean', default: false },
|
|
290
303
|
},
|
|
291
304
|
// The documented opt-out is `--no-native`; without allowNegative,
|
|
@@ -308,10 +321,7 @@ async function main() {
|
|
|
308
321
|
// headless caller can pipe this straight into stories-wave-tick.js.
|
|
309
322
|
routeAllOutputToStderr();
|
|
310
323
|
|
|
311
|
-
return runResolveStories(
|
|
312
|
-
{ ids: values.ids, native: values.native, pretty: values.pretty },
|
|
313
|
-
resolveStoriesProvider(),
|
|
314
|
-
);
|
|
324
|
+
return runResolveStories(toRunOptions(values), resolveStoriesProvider());
|
|
315
325
|
}
|
|
316
326
|
|
|
317
327
|
runAsCli(import.meta.url, main, {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* [--no-auto-merge]
|
|
45
45
|
* [--wait-merge | --no-wait-merge]
|
|
46
46
|
* [--merge-watch-mode <sync|async>]
|
|
47
|
+
* [--rerun-advisory <n>]
|
|
47
48
|
* [--override-review-block <reason>]
|
|
48
49
|
*
|
|
49
50
|
* `--override-review-block <reason>` is the one sanctioned way
|
|
@@ -225,6 +226,10 @@ runAsCli(import.meta.url, main, {
|
|
|
225
226
|
['--wait-merge', 'Force the in-close merge wait.'],
|
|
226
227
|
['--no-wait-merge', 'Return as soon as the PR is open; do not wait.'],
|
|
227
228
|
['--max-wait-seconds <n>', 'Per-invocation merge-wait bound.'],
|
|
229
|
+
[
|
|
230
|
+
'--rerun-advisory <n>',
|
|
231
|
+
'How many times this close may re-run a failed ADVISORY (non-required) workflow run before blocking on it. Overrides `delivery.ci.rerunAdvisory` for one invocation; BOTH default to 0, so close spends no CI minutes and issues no GitHub mutation on an advisory red unless you ask. At n > 0 the failed run(s) are re-run within that allowance and the merge wait keeps polling inside its existing budget, landing or blocking on the re-run verdict.',
|
|
232
|
+
],
|
|
228
233
|
[
|
|
229
234
|
'--merge-watch-mode <sync|async>',
|
|
230
235
|
'Override delivery.mergeWatch.mode for this invocation only. `async` caps the merge wait to a short probe window and returns the resumable `pending` terminal instead of holding the foreground slot — pass it on every close of a multi-Story run. An invalid value exits non-zero before any phase runs.',
|