mandrel 2.9.0 → 2.11.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/.markdownlint.json +4 -0
- package/.agents/agents/acceptance-critic.md +30 -5
- package/.agents/agents/auditor.md +36 -19
- package/.agents/agents/plan-critic.md +31 -5
- package/.agents/agents/story-worker.md +91 -100
- package/.agents/docs/configuration.md +39 -25
- package/.agents/docs/execution-reference.md +13 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +131 -265
- package/.agents/rules/git-conventions.md +47 -83
- package/.agents/rules/orchestration-error-handling.md +28 -0
- package/.agents/schemas/agentrc.schema.json +36 -9
- package/.agents/schemas/validation-evidence.schema.json +3 -1
- package/.agents/scripts/acceptance-eval.js +10 -6
- package/.agents/scripts/apply-quality-bootstrap.js +1 -1
- package/.agents/scripts/check-test-temp-hygiene.js +438 -0
- package/.agents/scripts/deliver-recover.js +23 -6
- package/.agents/scripts/lib/audit-suite/index.js +5 -0
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +179 -0
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/baselines/env-overrides.js +33 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/preview-gates.js +5 -0
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +10 -1
- package/.agents/scripts/lib/config/quality.js +13 -0
- package/.agents/scripts/lib/config/temp-paths.js +121 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +30 -0
- package/.agents/scripts/lib/config-settings-schema.js +12 -16
- package/.agents/scripts/lib/observability/metrics-ledger.js +217 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +7 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +45 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +97 -4
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +533 -93
- package/.agents/scripts/lib/orchestration/deliver-recover.js +137 -10
- package/.agents/scripts/lib/orchestration/merge-block-class.js +36 -15
- package/.agents/scripts/lib/orchestration/merge-poll.js +213 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -3
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +182 -9
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +29 -2
- package/.agents/scripts/lib/orchestration/plan-metrics.js +31 -82
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +153 -2
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +221 -14
- package/.agents/scripts/lib/orchestration/resolve-stories.js +12 -1
- package/.agents/scripts/lib/orchestration/review-depth.js +9 -4
- package/.agents/scripts/lib/orchestration/review-providers/native.js +34 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +230 -79
- package/.agents/scripts/lib/orchestration/spec-budget.js +78 -0
- package/.agents/scripts/lib/orchestration/story-body-gate.js +72 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +89 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +73 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +6 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -62
- package/.agents/scripts/lib/templates/decomposer-prompts.js +13 -6
- package/.agents/scripts/lib/test-env.js +65 -0
- package/.agents/scripts/plan-context.js +84 -9
- package/.agents/scripts/plan-critics.js +115 -3
- package/.agents/scripts/plan-persist.js +11 -1
- package/.agents/scripts/plan-run-epilogue.js +1 -1
- package/.agents/scripts/resolve-stories.js +2 -0
- package/.agents/scripts/single-story-confirm-merge.js +65 -5
- package/.agents/scripts/stories-wave-tick.js +1 -1
- package/.agents/workflows/deliver.md +88 -230
- package/.agents/workflows/helpers/acceptance-self-eval.md +16 -5
- package/.agents/workflows/helpers/deliver-reference.md +171 -0
- package/.agents/workflows/helpers/deliver-story-reference.md +223 -0
- package/.agents/workflows/helpers/deliver-story.md +115 -432
- package/.agents/workflows/helpers/plan-reference.md +246 -0
- package/.agents/workflows/plan.md +108 -304
- package/docs/CHANGELOG.md +46 -0
- package/lib/cli/registry.js +31 -14
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +92 -0
- package/package.json +1 -1
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* act on it — dispatching a fresh-context critic sub-agent and folding its
|
|
10
10
|
* findings into a re-author round **before** the plan is persisted.
|
|
11
11
|
*
|
|
12
|
+
* The pre-mortem's external-dependency arm (Story #4700) needs the repo's own
|
|
13
|
+
* manifests to tell an external scoped package from a local one, so this CLI
|
|
14
|
+
* reads them (`collectRepoPackages`) and passes the specifier set down — the
|
|
15
|
+
* pure evaluation modules never touch the filesystem.
|
|
16
|
+
*
|
|
12
17
|
* Why here and nowhere else. The evaluation used to run inside
|
|
13
18
|
* `run-plan-persist.js`, after authoring was finished and immediately before
|
|
14
19
|
* `createStoryIssues` — the one point in the flow where nothing can act on a
|
|
@@ -43,7 +48,7 @@
|
|
|
43
48
|
* Exit codes: 0 success (any verdict); 1 usage/IO error.
|
|
44
49
|
*/
|
|
45
50
|
|
|
46
|
-
import { readFile } from 'node:fs/promises';
|
|
51
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
47
52
|
import path from 'node:path';
|
|
48
53
|
import { parseArgs } from 'node:util';
|
|
49
54
|
|
|
@@ -63,6 +68,101 @@ const USAGE = 'Usage: plan-critics.js --stories <file> [--tech-spec <file>]';
|
|
|
63
68
|
/** The `cli` discriminator every ledger record from this surface carries. */
|
|
64
69
|
export const PLAN_CRITICS_CLI = 'plan-critics';
|
|
65
70
|
|
|
71
|
+
/** Dependency maps a manifest can declare a package under. */
|
|
72
|
+
const DEP_MAP_KEYS = [
|
|
73
|
+
'dependencies',
|
|
74
|
+
'devDependencies',
|
|
75
|
+
'optionalDependencies',
|
|
76
|
+
'peerDependencies',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/** Add a manifest's own name and every declared dependency to `names`. */
|
|
80
|
+
function collectPackageIdentity(names, pkg) {
|
|
81
|
+
if (typeof pkg?.name === 'string') names.add(pkg.name);
|
|
82
|
+
for (const key of DEP_MAP_KEYS) {
|
|
83
|
+
const map = pkg?.[key];
|
|
84
|
+
if (map && typeof map === 'object') {
|
|
85
|
+
for (const dep of Object.keys(map)) names.add(dep);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Best-effort JSON read: a missing or malformed file yields `null`. */
|
|
91
|
+
async function readJsonIfPresent(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Normalize the `workspaces` field (array or `{ packages: [] }`) to a list. */
|
|
100
|
+
function workspacePatterns(pkg) {
|
|
101
|
+
const ws = pkg?.workspaces;
|
|
102
|
+
if (Array.isArray(ws)) return ws;
|
|
103
|
+
if (Array.isArray(ws?.packages)) return ws.packages;
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve `workspaces` patterns to child `package.json` paths. Handles the two
|
|
109
|
+
* common shapes — a `dir/*` glob (expanded one level) and a literal path — and
|
|
110
|
+
* never throws: an unreadable base directory is skipped.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} rootDir
|
|
113
|
+
* @param {string[]} patterns
|
|
114
|
+
* @returns {Promise<string[]>}
|
|
115
|
+
*/
|
|
116
|
+
async function resolveWorkspaceManifestPaths(rootDir, patterns) {
|
|
117
|
+
const paths = [];
|
|
118
|
+
for (const pattern of patterns) {
|
|
119
|
+
if (typeof pattern !== 'string') continue;
|
|
120
|
+
if (!pattern.endsWith('/*')) {
|
|
121
|
+
paths.push(path.resolve(rootDir, pattern, 'package.json'));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const base = path.resolve(rootDir, pattern.slice(0, -2));
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = await readdir(base, { withFileTypes: true });
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
paths.push(path.join(base, entry.name, 'package.json'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return paths;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gather the package specifiers the repo's own manifests declare — the set the
|
|
142
|
+
* pre-mortem external-dependency probe (Story #4700) measures a scoped-package
|
|
143
|
+
* reference against. Includes the root manifest's own name and dependency maps
|
|
144
|
+
* plus every workspace manifest's. Best-effort: a repo with no `package.json`
|
|
145
|
+
* yields `[]`, which only widens what the probe treats as external.
|
|
146
|
+
*
|
|
147
|
+
* @param {{ rootDir?: string }} [opts]
|
|
148
|
+
* @returns {Promise<string[]>}
|
|
149
|
+
*/
|
|
150
|
+
export async function collectRepoPackages({ rootDir = process.cwd() } = {}) {
|
|
151
|
+
const root = await readJsonIfPresent(path.join(rootDir, 'package.json'));
|
|
152
|
+
if (!root) return [];
|
|
153
|
+
const names = new Set();
|
|
154
|
+
collectPackageIdentity(names, root);
|
|
155
|
+
const wsPaths = await resolveWorkspaceManifestPaths(
|
|
156
|
+
rootDir,
|
|
157
|
+
workspacePatterns(root),
|
|
158
|
+
);
|
|
159
|
+
for (const wsPath of wsPaths) {
|
|
160
|
+
const pkg = await readJsonIfPresent(wsPath);
|
|
161
|
+
if (pkg) collectPackageIdentity(names, pkg);
|
|
162
|
+
}
|
|
163
|
+
return [...names];
|
|
164
|
+
}
|
|
165
|
+
|
|
66
166
|
/**
|
|
67
167
|
* Read the draft artifacts the critics evaluate.
|
|
68
168
|
*
|
|
@@ -155,21 +255,32 @@ export async function recordCriticSkips(
|
|
|
155
255
|
* storiesPath: string,
|
|
156
256
|
* techSpecPath?: string|null,
|
|
157
257
|
* config?: object,
|
|
258
|
+
* knownPackages?: string[],
|
|
158
259
|
* append?: typeof appendCriticSkip,
|
|
159
260
|
* }} args
|
|
261
|
+
* @param {string[]} [args.knownPackages] - Package specifiers the repo's own
|
|
262
|
+
* manifests declare, forwarded to the pre-mortem external-dependency probe
|
|
263
|
+
* (Story #4700). `main()` resolves them via `collectRepoPackages`; tests may
|
|
264
|
+
* pass an explicit set or omit it (defaults to `[]`).
|
|
160
265
|
* @returns {Promise<{ consolidation: object, premortem: object, textHygiene: object }>}
|
|
161
266
|
*/
|
|
162
267
|
export async function evaluateCriticArtifacts({
|
|
163
268
|
storiesPath,
|
|
164
269
|
techSpecPath = null,
|
|
165
270
|
config = {},
|
|
271
|
+
knownPackages = [],
|
|
166
272
|
append = appendCriticSkip,
|
|
167
273
|
}) {
|
|
168
274
|
const { tickets, techSpecContent } = await loadCriticArtifacts({
|
|
169
275
|
storiesPath,
|
|
170
276
|
techSpecPath,
|
|
171
277
|
});
|
|
172
|
-
const verdict = evaluatePlanCritics({
|
|
278
|
+
const verdict = evaluatePlanCritics({
|
|
279
|
+
techSpecContent,
|
|
280
|
+
tickets,
|
|
281
|
+
config,
|
|
282
|
+
knownPackages,
|
|
283
|
+
});
|
|
173
284
|
await recordCriticSkips(verdict, config, { append });
|
|
174
285
|
return verdict;
|
|
175
286
|
}
|
|
@@ -191,9 +302,10 @@ async function main() {
|
|
|
191
302
|
? path.resolve(values['tech-spec'])
|
|
192
303
|
: null,
|
|
193
304
|
config: resolveConfig(),
|
|
305
|
+
knownPackages: await collectRepoPackages(),
|
|
194
306
|
});
|
|
195
307
|
|
|
196
|
-
process.stdout.write(`${JSON.stringify(verdict
|
|
308
|
+
process.stdout.write(`${JSON.stringify(verdict)}\n`);
|
|
197
309
|
return 0;
|
|
198
310
|
}
|
|
199
311
|
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
* ids, for hand-driven runs. Each id must be
|
|
35
35
|
* claimed by exactly one Story's `supersedes[]`;
|
|
36
36
|
* they are commented on and closed as superseded
|
|
37
|
+
* --route-downgrade-reason <text>
|
|
38
|
+
* Audited planner downgrade (Story #4707): treat
|
|
39
|
+
* the envelope's `full` complexity verdict as
|
|
40
|
+
* `lite`, recording <text> on every Story's
|
|
41
|
+
* story-plan-state checkpoint. Absent this flag
|
|
42
|
+
* the deterministic verdict stands; the gate
|
|
43
|
+
* itself still fails toward `full`
|
|
37
44
|
* --no-close-superseded Keep the source tickets open (no comment, no
|
|
38
45
|
* close) — for a genuinely partial supersede
|
|
39
46
|
* --dry-run Assemble + validate without GitHub writes
|
|
@@ -100,6 +107,7 @@ const CLI_OPTIONS = {
|
|
|
100
107
|
'plan-context': { type: 'string' },
|
|
101
108
|
'plan-acceptance': { type: 'string' },
|
|
102
109
|
'source-tickets': { type: 'string' },
|
|
110
|
+
'route-downgrade-reason': { type: 'string' },
|
|
103
111
|
'close-superseded': { type: 'boolean', default: true },
|
|
104
112
|
'no-close-superseded': { type: 'boolean', default: false },
|
|
105
113
|
'dry-run': { type: 'boolean', default: false },
|
|
@@ -113,6 +121,7 @@ const USAGE =
|
|
|
113
121
|
'[--tech-spec <file>] [--plan-dir <dir>] [--plan-context <file>] ' +
|
|
114
122
|
'[--plan-acceptance <file>] ' +
|
|
115
123
|
'[--source-tickets <ids>] [--no-close-superseded] ' +
|
|
124
|
+
'[--route-downgrade-reason <text>] ' +
|
|
116
125
|
'[--dry-run] [--force-review] ' +
|
|
117
126
|
'[--allow-over-budget] [--allow-large-fan-out]';
|
|
118
127
|
|
|
@@ -204,6 +213,7 @@ export function buildPersistOptions(values, paths, planContextEnvelope) {
|
|
|
204
213
|
skipCleanup: values['dry-run'],
|
|
205
214
|
sourceTicketIds: source.ids,
|
|
206
215
|
sourceTicketOrigin: source.origin,
|
|
216
|
+
routeDowngradeReason: values['route-downgrade-reason'] ?? null,
|
|
207
217
|
// Default-on: `--no-close-superseded` is the explicit escape and always
|
|
208
218
|
// wins over the (default `true`) `--close-superseded`.
|
|
209
219
|
closeSuperseded:
|
|
@@ -328,7 +338,7 @@ async function main() {
|
|
|
328
338
|
|
|
329
339
|
await attachPlanMetrics(result, config, metricsSince);
|
|
330
340
|
|
|
331
|
-
process.stdout.write(`${JSON.stringify(result
|
|
341
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
332
342
|
}
|
|
333
343
|
|
|
334
344
|
runAsCli(import.meta.url, main, { source: 'plan-persist' });
|
|
@@ -67,7 +67,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
67
67
|
});
|
|
68
68
|
warnOnUnresolvedBase(result);
|
|
69
69
|
warnOnEmptyRollup(result);
|
|
70
|
-
Logger.info(JSON.stringify(result
|
|
70
|
+
Logger.info(JSON.stringify(result));
|
|
71
71
|
if (result.errors?.length) {
|
|
72
72
|
process.exitCode = 1;
|
|
73
73
|
}
|
|
@@ -209,6 +209,7 @@ async function main() {
|
|
|
209
209
|
stories,
|
|
210
210
|
nativeEdges,
|
|
211
211
|
warn: (m) => Logger.warn(m),
|
|
212
|
+
config,
|
|
212
213
|
});
|
|
213
214
|
const foreignDone = await resolveForeignDone({
|
|
214
215
|
provider,
|
|
@@ -220,6 +221,7 @@ async function main() {
|
|
|
220
221
|
nativeEdges,
|
|
221
222
|
foreignDone,
|
|
222
223
|
warn: () => {},
|
|
224
|
+
config,
|
|
223
225
|
});
|
|
224
226
|
|
|
225
227
|
process.stdout.write(
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* short-circuits to a `noop` envelope.
|
|
28
28
|
*
|
|
29
29
|
* Usage:
|
|
30
|
-
* node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>]
|
|
30
|
+
* node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait]
|
|
31
|
+
* [--max-wait-seconds <n>]
|
|
31
32
|
* [--cwd <main-repo>]
|
|
32
33
|
*
|
|
33
34
|
* Exit codes: 0 ok (merged, pending, or noop), 1 error.
|
|
@@ -40,12 +41,13 @@ import { parseSprintArgs } from './lib/cli-args.js';
|
|
|
40
41
|
import { runAsCli } from './lib/cli-utils.js';
|
|
41
42
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
42
43
|
import { formatCliError } from './lib/error-redactor.js';
|
|
43
|
-
import {
|
|
44
|
+
import { createGh } from './lib/gh-exec.js';
|
|
44
45
|
import { getStoryBranch } from './lib/git-utils.js';
|
|
45
46
|
import { Logger } from './lib/Logger.js';
|
|
46
47
|
import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
|
|
47
48
|
import { emitTerseResult } from './lib/observability/terse-result.js';
|
|
48
49
|
import { MERGED_FLIP_FAILED_BLOCK_CLASS } from './lib/orchestration/lifecycle/emit-merge-flip-failed.js';
|
|
50
|
+
import { MERGE_WAIT_GH_TIMEOUT_MS } from './lib/orchestration/merge-poll.js';
|
|
49
51
|
import { parsePrNumber } from './lib/orchestration/single-story-close/phases/code-review.js';
|
|
50
52
|
import { runConfirmMergePhase as defaultRunConfirmMergePhase } from './lib/orchestration/single-story-close/phases/confirm-merge.js';
|
|
51
53
|
import { parseCloseOptions } from './lib/orchestration/single-story-close/phases/options.js';
|
|
@@ -64,6 +66,25 @@ const progress = Logger.createProgress('single-story-confirm-merge', {
|
|
|
64
66
|
stderr: true,
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Default `gh` facade for this CLI, bound to the merge wait's spawn-level
|
|
71
|
+
* timeout (Story #4710). This CLI is the resume surface async mode hands the
|
|
72
|
+
* merge wait to — a background invocation with no host tool ceiling — so an
|
|
73
|
+
* un-timeboxed `gh pr list` / `gh pr view` here could strand the resume the
|
|
74
|
+
* same way an un-timeboxed probe stranded the in-close wait.
|
|
75
|
+
*/
|
|
76
|
+
const defaultGh = createGh(undefined, { timeoutMs: MERGE_WAIT_GH_TIMEOUT_MS });
|
|
77
|
+
|
|
78
|
+
/** One usage string for the throw path and `--help` (Story #4710). */
|
|
79
|
+
const USAGE =
|
|
80
|
+
'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait] ' +
|
|
81
|
+
'[--max-wait-seconds <n>] [--cwd <main-repo>]\n\n' +
|
|
82
|
+
' --wait resume the bounded merge wait instead of probing once\n' +
|
|
83
|
+
' --max-wait-seconds per-invocation wait bound override, threaded to\n' +
|
|
84
|
+
' resolveMergeWaitConfig exactly as the close does (wins\n' +
|
|
85
|
+
' over delivery.mergeWatch.maxWaitSeconds and the async\n' +
|
|
86
|
+
' probe-window cap; only meaningful with --wait)';
|
|
87
|
+
|
|
67
88
|
/**
|
|
68
89
|
* Read the `--pr <n>` flag from `process.argv` for the direct-CLI path.
|
|
69
90
|
* Injection callers pass `pr` directly and never reach this. Returns the
|
|
@@ -106,6 +127,33 @@ function readWaitFlag() {
|
|
|
106
127
|
}
|
|
107
128
|
}
|
|
108
129
|
|
|
130
|
+
/**
|
|
131
|
+
* `--max-wait-seconds <n>`: per-invocation wait-bound override for the
|
|
132
|
+
* `--wait` resume path (Story #4710). The close CLI already accepted this
|
|
133
|
+
* flag, but the resume CLI — the exact command async mode's `pending`
|
|
134
|
+
* terminal hands off to — did not, so the documented per-run override was
|
|
135
|
+
* unreachable where it mattered most and a slow-CI landing depended on an
|
|
136
|
+
* unbounded chain of short invocations. Threaded to `runConfirmMergePhase`
|
|
137
|
+
* (and thence `resolveMergeWaitConfig`) exactly as close threads its own
|
|
138
|
+
* flag. Returns `undefined` when absent or not a positive integer — the
|
|
139
|
+
* phase's config/default resolution owns that case.
|
|
140
|
+
*
|
|
141
|
+
* @returns {number|undefined}
|
|
142
|
+
*/
|
|
143
|
+
function readMaxWaitSecondsFlag() {
|
|
144
|
+
try {
|
|
145
|
+
const { values } = parseArgs({
|
|
146
|
+
args: process.argv.slice(2),
|
|
147
|
+
options: { 'max-wait-seconds': { type: 'string' } },
|
|
148
|
+
strict: false,
|
|
149
|
+
});
|
|
150
|
+
const parsed = Number.parseInt(String(values['max-wait-seconds']), 10);
|
|
151
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
109
157
|
/**
|
|
110
158
|
* Resolve the PR number for the Story branch when one was not passed on
|
|
111
159
|
* the CLI. Probes `gh pr list --head <branch> --state all` (the merged PR
|
|
@@ -262,6 +310,7 @@ export async function runConfirmMerge({
|
|
|
262
310
|
cwd: cwdParam,
|
|
263
311
|
pr: prParam,
|
|
264
312
|
wait: waitParam,
|
|
313
|
+
maxWaitSeconds: maxWaitSecondsParam,
|
|
265
314
|
injectedProvider,
|
|
266
315
|
injectedConfig,
|
|
267
316
|
injectedGh,
|
|
@@ -275,11 +324,10 @@ export async function runConfirmMerge({
|
|
|
275
324
|
});
|
|
276
325
|
|
|
277
326
|
if (!storyId) {
|
|
278
|
-
throw new Error(
|
|
279
|
-
'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait] [--cwd <main-repo>]',
|
|
280
|
-
);
|
|
327
|
+
throw new Error(USAGE);
|
|
281
328
|
}
|
|
282
329
|
const wait = waitParam ?? readWaitFlag();
|
|
330
|
+
const maxWaitSeconds = maxWaitSecondsParam ?? readMaxWaitSecondsFlag();
|
|
283
331
|
|
|
284
332
|
const startedAtMs = Date.now();
|
|
285
333
|
const config = injectedConfig || resolveConfig({ cwd });
|
|
@@ -346,6 +394,10 @@ export async function runConfirmMerge({
|
|
|
346
394
|
// The close already armed it; this CLI is resuming that wait, not
|
|
347
395
|
// deciding whether to arm.
|
|
348
396
|
autoMergeEnabled: true,
|
|
397
|
+
// The per-run override (`--max-wait-seconds`), resolved by
|
|
398
|
+
// `resolveMergeWaitConfig` exactly as the close resolves its own flag —
|
|
399
|
+
// it wins over the config value and the async probe-window cap.
|
|
400
|
+
maxWaitSeconds,
|
|
349
401
|
provider,
|
|
350
402
|
config,
|
|
351
403
|
progress,
|
|
@@ -450,6 +502,14 @@ export async function runConfirmMerge({
|
|
|
450
502
|
* or none at all; "none at all" is what Story #4543 removes.
|
|
451
503
|
*/
|
|
452
504
|
async function main() {
|
|
505
|
+
if (process.argv.includes('--help')) {
|
|
506
|
+
// Print usage (including --max-wait-seconds) and exit cleanly — the
|
|
507
|
+
// resume-path override is only discoverable if the CLI can say it exists.
|
|
508
|
+
// `process.stdout.write` (not console.log) keeps the CLI within the
|
|
509
|
+
// no-console repo invariant while still writing help to stdout.
|
|
510
|
+
process.stdout.write(`${USAGE}\n`);
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
453
513
|
try {
|
|
454
514
|
const outcome = await runConfirmMerge();
|
|
455
515
|
return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
|