From aac203c4bc1e613023caae0eb56dcca3edce591b Mon Sep 17 00:00:00 2001 From: os-musk Date: Mon, 7 Sep 2026 22:35:25 +0000 Subject: [PATCH] fix(pm): check-clause2-carriers names the board's SOURCE, answers --help, and refuses an argument it does not honour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The board was already a parameter — `resolveSweepRepo` (imported from check-half-states.mjs) resolves PM_SWEEP_REPO, else GITHUB_REPOSITORY, else the default, and returns `source` alongside `repo`. The run simply never printed `source`, so a deliberate target and the fallback rendered as the same sentence, and a seat reading only this script concluded the board was hard-scoped. Three changes, one file: 1. Every run opens with a provenance line naming the board AND on what basis it is the board, on stderr in every mode (stdout stays contractually the answer), printed before the first request so a run that dies in transport has still said which board it was about. 2. `--help` / `-h` print usage and exit 0 before any network read. The usage text carries the env precedence, so the one action a seat takes to discover the mechanism no longer hides it behind a sweep. 3. An argument this file does not honour is refused before any read, naming it and printing the usage, exiting 1 — the register's usage code, deliberately not dispatch-gates' 2, which here is the UNJUDGED verdict. A KNOWN_FLAGS table is the single source for both the parse and the usage text: every parse site looks its flag up through `flagIndex`, which throws on a flag absent from the table, so a flag the tool honours and does not document is unreachable rather than merely unlikely. A repo-shaped positional is pointed at PM_SWEEP_REPO — the confusion this change exists to end. No verdict, row, reader or `--pair` semantics changed. 42 self-test cases added (251 pass, was 209). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX --- scripts/pm/check-clause2-carriers.mjs | 335 +++++++++++++++++++++++++- 1 file changed, 327 insertions(+), 8 deletions(-) diff --git a/scripts/pm/check-clause2-carriers.mjs b/scripts/pm/check-clause2-carriers.mjs index 71d7d6053c..a7dfc0e225 100644 --- a/scripts/pm/check-clause2-carriers.mjs +++ b/scripts/pm/check-clause2-carriers.mjs @@ -11,6 +11,24 @@ * node scripts/pm/check-clause2-carriers.mjs --pair 13910 # ONE PR: a pre-arm predicate * node scripts/pm/check-clause2-carriers.mjs --json # the sweep, for round reports * node scripts/pm/check-clause2-carriers.mjs --self-test # offline, no network + * node scripts/pm/check-clause2-carriers.mjs --help # usage; no network + * + * ## Which board this answers about, and how a reader can tell (#16623) + * + * The board is a PARAMETER, resolved once per run by `resolveSweepRepo` + * (imported): `PM_SWEEP_REPO`, else `GITHUB_REPOSITORY`, else the default. It + * is not a property of this checkout -- this file reads no file in the tree at + * all, so an environment variable really does retarget it, and a sibling repo's + * seat runs `PM_SWEEP_REPO= node scripts/pm/check-clause2-carriers.mjs + * --pair N` to get an answer about its own board. + * + * ⚠️ That was TRUE before this file said so, and saying so is the fix (#16623). + * The filing seat grepped THIS file for a repo flag, found nothing, and + * concluded the capability was absent -- while `resolveSweepRepo` sat in the + * import block eight lines from where it stopped reading. The resolver already + * returns `source` alongside `repo`; the run simply never printed it, so a + * DELIBERATE target and the DEFAULT fallback rendered identically. Every run + * now opens with a provenance line that names both. * * # the SAME predicate with no private token and no network at all — the pair * # pre-fetched into a document, named as a file or fed on stdin: @@ -257,7 +275,13 @@ * not a fact about whichever PR happens to run CI next, so failing a build * over it would punish the wrong actor (`check-half-states.mjs`'s header * argues this at length, and this file is the same family). - * 1 bad usage — could not sweep at all. + * 1 bad usage — could not sweep at all. Since #16623 this also covers an + * argument this file does not honour: it is refused BEFORE any read, so + * the refusal is about the invocation and never about a board. + * ⛔ Deliberately NOT 2, which `dispatch-gates.mjs` uses for the same + * refusal — 2 here is a VERDICT (UNJUDGED), and a seat reading `$?` must + * never be able to read a mistyped invocation as an incomplete sweep. + * `--help` is not an error: it answers and exits 0, also before any read. * 2 swept, but INCOMPLETE: at least one pair whose card, labels or comments * could not be read. An unread carrier is NOT a bare carrier and an unread * thread is NOT an absent declaration (#4690): incomplete must never read @@ -310,8 +334,10 @@ import { isEntrypoint } from '../invoked-as.mjs'; import { CLAIM_COMMENT_MARKER, CONTRACT_REVIEW_LABEL, + DEFAULT_SWEEP_REPO, EXIT_PREREQUISITE_NOT_MET, PROXY_FLAG, + SWEEP_REPO_SHAPE, governingClaim, isGateSemanticLabel, labelNames, @@ -359,11 +385,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'the three read paths: ordered, offline-capable, and named in every refusal': 24, 'C5: the direction claim checked against the diff (#16448)': 16, 'the exit register is distinct in every direction it must be': 6, + 'the argv contract and the board provenance (#16623)': 42, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 13; +const SELF_TEST_BATTERY_FLOOR = 14; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -2400,6 +2427,76 @@ export function selfTest() { t('…its INCOMPLETE is this file\'s INCOMPLETE', WT_EXIT_INCOMPLETE === EXIT_INCOMPLETE); t('…and its OK and USAGE agree too', WT_EXIT_OK === EXIT_OK && WT_EXIT_USAGE === EXIT_USAGE); + // -- the argv contract and the board provenance (#16623) ------------------- + // + // The card that filed this was itself the failure it describes: its author + // grepped THIS file for a repo flag, found nothing, and concluded the + // capability was absent -- while `resolveSweepRepo` sat in the import block. + // So the cases below pin the two halves that would have answered them: the + // board says on what basis it is the board, and an argument this file does + // not honour is refused instead of silently changing nothing. + battery('the argv contract and the board provenance (#16623)'); + const refusalText = (argv) => (argvRefusalLines(argv) ?? []).join('\n'); + const usageText = usageLines().join('\n'); + const THIS_BOARD = 'objectstack-ai/objectstack'; + const OTHER_BOARD = 'objectstack-ai/objectui'; + + // -- (2) --help / -h answer, and answer BEFORE anything is read ------------ + t('--help is recognised', wantsHelp(['--help']) === true); + t('…and the short spelling seats actually type', wantsHelp(['-h']) === true); + t('⛔ a bare run is NOT a help request — it is the sweep', wantsHelp([]) === false); + t('⛔ nor is any other honoured flag', wantsHelp(['--json']) === false && wantsHelp(['--pair', '13910']) === false); + t('⛔ and help is not refused as an unknown argument', argvRefusalLines(['--help']) === null && argvRefusalLines(['-h']) === null); + t('the usage text names every flag this file honours, rendered from the table', [...KNOWN_FLAGS.keys()].every((f) => usageText.includes(f))); + t('…and it names the env var that retargets the board — the fact the filing seat could not find', usageText.includes('PM_SWEEP_REPO')); + t('…and the runner variable beside it, so the precedence is legible', usageText.includes('GITHUB_REPOSITORY')); + t('…and the fallback board is the sibling\'s constant, imported rather than retyped', usageText.includes(DEFAULT_SWEEP_REPO)); + t('help exits OK — a question answered is not an error', EXIT_OK === 0); + + // -- (1) the board says on what basis it is the board ---------------------- + t('the default fallback is named AS a fallback', says(boardProvenanceLine({ repo: THIS_BOARD, source: 'default' }), 'source: default')); + t('…and carries the action that changes it, which is the whole finding', says(boardProvenanceLine({ repo: THIS_BOARD, source: 'default' }), 'set PM_SWEEP_REPO to target another repo')); + t('an explicit target names PM_SWEEP_REPO as the source', says(boardProvenanceLine({ repo: OTHER_BOARD, source: 'PM_SWEEP_REPO' }), 'source: PM_SWEEP_REPO')); + t('…and a runner names GITHUB_REPOSITORY', says(boardProvenanceLine({ repo: OTHER_BOARD, source: 'GITHUB_REPOSITORY' }), 'source: GITHUB_REPOSITORY')); + t('the board itself is in the line in every case', says(boardProvenanceLine({ repo: OTHER_BOARD, source: 'PM_SWEEP_REPO' }), OTHER_BOARD)); + t('⭐ a deliberate target and the fallback are DIFFERENT lines — the defect, in one assertion', boardProvenanceLine({ repo: THIS_BOARD, source: 'default' }) !== boardProvenanceLine({ repo: THIS_BOARD, source: 'PM_SWEEP_REPO' })); + // Driven through the real resolver, not a hand-built shape: the line and the + // resolution cannot drift apart, and `source` is proven to be what it renders. + t('the line is fed by resolveSweepRepo itself, so the two cannot disagree', says(boardProvenanceLine(resolveSweepRepo({ PM_SWEEP_REPO: OTHER_BOARD })), OTHER_BOARD) && says(boardProvenanceLine(resolveSweepRepo({ PM_SWEEP_REPO: OTHER_BOARD })), 'PM_SWEEP_REPO')); + t('…and a bare environment renders the default leg through that same resolver', says(boardProvenanceLine(resolveSweepRepo({})), 'source: default')); + + // -- (3) an argument this file does not honour is REFUSED ------------------ + t('an unknown flag is REFUSED rather than ignored', argvRefusalLines(['--this-flag-does-not-exist']) !== null); + t('…and the refusal NAMES it', says(refusalText(['--this-flag-does-not-exist']), '--this-flag-does-not-exist')); + t('…the filing card\'s own probe — an unknown flag with a value, beside a real one — names the unknown one', says(refusalText(['--issue', '7760', '--json']), '--issue')); + t('…and every refusal prints what IS honoured, so the next attempt can be right', [...KNOWN_FLAGS.keys()].every((f) => says(refusalText(['--nope']), f))); + t('…including the env precedence, which is where the filing seat\'s question actually lived', says(refusalText(['--nope']), 'PM_SWEEP_REPO')); + t('a joined value spelling is refused, showing the spelling this file takes', says(refusalText([`--pair=13910`]), '--pair 13910')); + t('a stray positional is refused too — this file reads none', says(refusalText(['16623']), '16623')); + t('…and is pointed at the flag a PR number belongs to', says(refusalText(['16623']), '--pair')); + t('⭐ but a repo-SHAPED positional is pointed at the mechanism that really retargets the board', says(refusalText([OTHER_BOARD]), `set PM_SWEEP_REPO='${OTHER_BOARD}'`)); + t('…which is exactly what the filing seat reached for and could not find', says(refusalText(['--repo', OTHER_BOARD]), 'PM_SWEEP_REPO')); + t('⛔ two bad arguments are both named, never just the first', says(refusalText(['--a', '--b']), '--a') && says(refusalText(['--a', '--b']), '--b')); + + // -- every honoured invocation still parses, unchanged --------------------- + t('a bare run is not refused', argvRefusalLines([]) === null); + t('--self-test still parses', argvRefusalLines(['--self-test']) === null); + t('--pair N still parses, and N is its VALUE rather than a stray positional', argvRefusalLines(['--pair', '13910']) === null); + t('--pair-json still parses', argvRefusalLines(['--pair', '13910', '--pair-json', 'pair.json']) === null); + t('…and its stdin spelling is a value, not a flag', argvRefusalLines(['--pair', '13910', '--pair-json', '-']) === null); + t('--json still parses', argvRefusalLines(['--json']) === null); + t('every honoured flag together still parses', argvRefusalLines(['--pair', '13910', '--pair-json', '-', '--json']) === null); + t('⛔ a value-taking flag whose value is MISSING never eats the flag after it', argvRefusalLines(['--pair', '--json']) === null); + + // -- the table is what the parse reads, so the two cannot drift ------------ + t('a parse site looks its flag up in the table', flagIndex(['--pair', '1'], '--pair') === 0 && flagIndex([], '--json') === -1); + t('⛔ and a lookup for a flag NOT in the table throws rather than answering', (() => { try { flagIndex([], '--not-in-the-table'); return false; } catch { return true; } })()); + t('the usage line is rendered from the table, values and all', usageText.includes('--pair-json path')); + + // -- the refusal's exit, against the pinned register ----------------------- + t('an argv refusal exits USAGE, never a verdict code', EXIT_USAGE !== EXIT_OK && EXIT_USAGE !== EXIT_INCOMPLETE && EXIT_USAGE !== EXIT_PAIR_ADVERSE && EXIT_USAGE !== EXIT_PREREQUISITE_NOT_MET); + t('⛔ and NOT dispatch-gates\' usage exit of 2, which is this file\'s UNJUDGED verdict', EXIT_USAGE === 1 && EXIT_INCOMPLETE === 2); + // -- The floor: every declared battery RAN, and ran its cases (#13489) ----- // // Evaluated after every battery has had its chance and BEFORE the verdict, so @@ -2455,17 +2552,208 @@ export function selfTest() { 'declaration states with the two not-read readings kept apart — no claim comment, and a claim ' + 'comment with no line — the 2026-08-31 seven-pair replay, the four gate-binding states ' + 'replayed from the 2026-09-01 clear, the verdict-authorship pair and its legacy silence, ' + - 'the three read paths with their offline reader, and the exit register).', + 'the three read paths with their offline reader, the argv contract with its usage and its ' + + 'refusal, the board provenance line, and the exit register).', ); selfTestReachedVerdict = true; return 0; } +// --------------------------------------------------------------------------- +// The argv contract, and the board's provenance (#16623) +// --------------------------------------------------------------------------- + +/** + * Every flag this file honours, what its value is spelled as, and what it does. + * + * ⭐ ONE table, and it is why the three things that used to disagree cannot: + * the parse looks every flag up through `flagIndex`, which THROWS on a flag + * that is not here; the refusal below decides what is an argument by asking it; + * and the usage text is rendered from it rather than typed beside it. A flag + * added to the parse without an entry here fails in this file's own self-test, + * instead of becoming a spelling the tool honours in one place and refuses in + * another. + * + * `value` is the value's spelling in the usage line, or `null` for a flag that + * takes none -- which is also how the refusal knows whether the NEXT argument + * belongs to this flag or is an argument of its own. + */ +export const KNOWN_FLAGS = new Map([ + ['--help', { value: null, does: 'print this text and exit 0, without reading any board' }], + ['-h', { value: null, does: 'the same' }], + ['--self-test', { value: null, does: "run this file's own battery, offline -- no board is read" }], + ['--pair', { value: 'N', does: 'judge ONE open PR by number -- a predicate about that pair' }], + ['--pair-json', { value: 'path', does: 'read the pair from a document (`-` = stdin) instead of the network' }], + ['--json', { value: null, does: 'emit the sweep on stdout as JSON' }], +]); + +/** How a flag and its value are spelled together, for the usage text. */ +function flagSpelling(flag, value) { + return value === null ? flag : `${flag} ${value}`; +} + +/** + * The usage text -- rendered from the table above and from the resolver's own + * precedence, so neither can drift from what the tool actually does. + * + * ⭐ The env paragraph is the half #16623 is about. A seat reading only this + * script used to have no way to learn that the board is a parameter at all. + */ +export function usageLines() { + return [ + 'usage: node scripts/pm/check-clause2-carriers.mjs ' + + [...KNOWN_FLAGS].map(([flag, { value }]) => `[${flagSpelling(flag, value)}]`).join(' '), + '', + ...[...KNOWN_FLAGS].map(([flag, { value, does }]) => ` ${flagSpelling(flag, value).padEnd(18)} ${does}`), + '', + ' Which board is read is resolved PER RUN, not from this checkout -- this file reads no file in', + ' the tree at all. The precedence is `resolveSweepRepo`\'s, imported from check-half-states.mjs:', + '', + ...[ + ['PM_SWEEP_REPO=owner/name', "an explicit target; a sibling repo's seat uses this"], + ['GITHUB_REPOSITORY', 'what Actions sets, i.e. the repo the workflow is installed in'], + [DEFAULT_SWEEP_REPO, 'the fallback, for a bare terminal'], + ].map(([source, why], i, all) => { + // Width derived from the entries, so the imported default cannot outgrow + // a hardcoded column and run into its own description. + const w = Math.max(...all.map(([s2]) => s2.length)) + 2; + return ` ${i + 1}. ${source.padEnd(w)}${why}`; + }), + '', + ' Every run prints which of the three answered, so a deliberate target and the fallback are', + ' never the same line. ⛔ A report about the wrong board reads exactly like a report about this one.', + ]; +} + +/** Did the caller ask for usage? Read through the table, like every other flag. */ +export function wantsHelp(argv) { + return flagIndex(argv, '--help') !== -1 || flagIndex(argv, '-h') !== -1; +} + +/** + * Where `flag` sits in argv, or -1 -- the ONE lookup every parse site uses. + * + * The throw is the point. `argv.indexOf('--pair')` is correct and unguarded: it + * will as happily find a flag the refusal and the usage text have never heard + * of, which is exactly how a file grows a flag it honours and does not + * document. Routing every lookup through the table makes that unreachable + * rather than merely unlikely. + */ +export function flagIndex(argv, flag) { + if (!KNOWN_FLAGS.has(flag)) { + throw new Error( + `check-clause2-carriers: the parse asked for '${flag}', which is not in KNOWN_FLAGS. That table ` + + 'is what the usage text and the unrecognised-argument refusal are derived from, so a flag ' + + 'missing from it is one this file would honour in silence and refuse in the same run. Add it there.', + ); + } + return argv.indexOf(flag); +} + +/** + * Every argument this file does not honour -- or `null` when there is none. + * + * ## Why an unrecognised argument is a REFUSAL and not a warning + * + * Measured on the filing seat's container, where the board could be read: + * `--this-flag-does-not-exist` produced a full, well-formed report about this + * file's own board and exited 0 -- byte-identical to a bare run. So a typo and + * a deliberate invocation were the same command, and `--help` -- the one thing + * a seat types to find out what the flags ARE -- ran a network sweep instead of + * answering. + * + * That matters more here than on a dispatch-time tool. This is a LANDING + * pre-check with nothing behind it: its whole purpose is to show that a review + * gate was CLEARED rather than STRIPPED, and 「闸门被剥不是红灯是放行」 -- so a + * well-formed `0 pair(s) UNJUDGED` reads exactly like a clean board whether or + * not the caller's argument meant anything. ⭐ An impossible-to-fail reading is + * indistinguishable from a reading that passed. + * + * ⛔ A positional argument is refused on the same ground and not a softer one: + * this file reads NO positional argument at all, so `… 16623` is a number the + * tool never looked at, above a full-board report that exits 0. + */ +export function argvRefusalLines(argv) { + const unknownFlags = []; + const positionals = []; + const joined = []; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (KNOWN_FLAGS.has(arg)) { + if (KNOWN_FLAGS.get(arg).value === null) continue; + const next = argv[i + 1]; + // Consume the value ONLY when one is really there. A missing or + // flag-shaped value is the flag's own site's refusal to report, with its + // own message; swallowing the next token here would hide that flag from + // this pass and name the wrong argument as the wrong one. + if (next !== undefined && (next === '-' || !next.startsWith('-'))) i++; + continue; + } + const eq = arg.indexOf('='); + if (eq > 0 && KNOWN_FLAGS.has(arg.slice(0, eq))) joined.push(arg); + else if (arg.startsWith('-')) unknownFlags.push(arg); + else positionals.push(arg); + } + const named = [...unknownFlags, ...joined, ...positionals]; + if (named.length === 0) return null; + + const lines = [ + `check-clause2-carriers: REFUSING — ${named.map((a) => `'${a}'`).join(', ')} ` + + `${named.length === 1 ? 'is not an argument' : 'are not arguments'} this file honours.`, + ]; + for (const flag of joined) { + const at = flag.indexOf('='); + lines.push( + ` '${flag}' spells its value with '='. Every value here is the NEXT argument: ` + + `'${flag.slice(0, at)} ${flag.slice(at + 1)}'.`, + ); + } + for (const arg of positionals) { + // ⭐ A value shaped like a repo gets pointed at the mechanism that actually + // retargets the board, because that is the confusion this whole change is + // about: the filing seat reached for `--repo /`, and the + // answer it needed was an environment variable it never saw named. + lines.push( + SWEEP_REPO_SHAPE.test(arg) + ? ` '${arg}' looks like a repo. The board is not an argument — set PM_SWEEP_REPO='${arg}' to target it.` + : ` '${arg}' is not a flag, and this file reads no positional argument at all — a PR number goes to '--pair'.`, + ); + } + lines.push( + ' ⛔ Not a warning: an unrecognised argument used to change NOTHING — same report, same board, same exit —', + ' so a typo and a deliberate invocation were the same command. This is a landing pre-check with nothing', + ' behind it, and a well-formed report reads exactly like a clean one whatever you meant to ask for.', + '', + ...usageLines(), + ); + return lines; +} + +/** + * WHICH board this run reads, and ON WHAT BASIS -- the line #16623 exists for. + * + * `resolveSweepRepo` has always returned `source` beside `repo`; the run simply + * never printed it, so `PM_SWEEP_REPO=objectstack-ai/objectui` and a bare + * terminal falling back to the default rendered as the same sentence. The value + * was already computed. This renders it. + * + * ⚠️ It goes to STDERR in every mode, `--json` included, for the reason the read + * path report next door states: stdout is contractually the ANSWER, and a + * provenance line on stdout would travel into the round report that pastes it + * as though it were part of the finding. + */ +export function boardProvenanceLine({ repo, source }) { + const detail = source === 'default' + ? 'source: default — set PM_SWEEP_REPO to target another repo' + : `source: ${source}`; + return `check-clause2-carriers: every row below is read from ${repo} (${detail}).`; +} + // --------------------------------------------------------------------------- async function main(argv) { - if (argv.includes('--self-test')) { + if (flagIndex(argv, '--self-test') !== -1) { const selfTestCode = selfTest(); if (!selfTestReachedVerdict) { console.error( @@ -2478,6 +2766,20 @@ async function main(argv) { return selfTestCode; } + // ⭐ Usage and argv BEFORE the board, and both before any read. A caller + // asking what the flags are must not be answered with a network sweep, and an + // unrecognised argument must be refused while the refusal can still be about + // the argument rather than about a board nobody asked for (#16623). + if (wantsHelp(argv)) { + for (const line of usageLines()) console.log(line); + return EXIT_OK; + } + const argvRefusal = argvRefusalLines(argv); + if (argvRefusal) { + for (const line of argvRefusal) console.error(line); + return EXIT_USAGE; + } + const repoRes = resolveSweepRepo(process.env); if (!repoRes.valid) { console.error( @@ -2494,8 +2796,11 @@ async function main(argv) { return EXIT_USAGE; } const repo = repoRes.repo; + // Printed before the first request, so a run that dies in transport has still + // said which board it was about -- the state the filing seat was in. + console.error(boardProvenanceLine(repoRes)); - const pairFlag = argv.indexOf('--pair'); + const pairFlag = flagIndex(argv, '--pair'); let only = null; if (pairFlag !== -1) { only = Number(argv[pairFlag + 1]); @@ -2505,7 +2810,7 @@ async function main(argv) { } } - const pairJsonFlag = argv.indexOf('--pair-json'); + const pairJsonFlag = flagIndex(argv, '--pair-json'); let reader = NETWORK_READER; if (pairJsonFlag !== -1) { const named = argv[pairJsonFlag + 1]; @@ -2568,7 +2873,7 @@ async function main(argv) { } return worst; } - return renderSweep({ repo, pulls, pairs }, { json: argv.includes('--json') }); + return renderSweep({ repo, pulls, pairs }, { json: flagIndex(argv, '--json') !== -1 }); } catch (err) { return reportTransportFailure(err, { swept }); } finally { @@ -2581,7 +2886,7 @@ async function main(argv) { } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) { + if (flagIndex(process.argv.slice(2), '--self-test') !== -1) { const selfTestCode = selfTest(); if (!selfTestReachedVerdict) { console.error( @@ -2593,6 +2898,20 @@ if (isEntrypoint(import.meta.url)) { } process.exit(selfTestCode); } else { + // ⭐ Answered HERE, above the proxy re-exec, so neither usage nor a refused + // argument spawns a child process or opens a socket. `main` judges both + // again -- the same two functions -- so the guard cannot be lost by a + // caller that reaches `main` another way. + const cliArgv = process.argv.slice(2); + if (wantsHelp(cliArgv)) { + for (const line of usageLines()) console.log(line); + process.exit(EXIT_OK); + } + const cliRefusal = argvRefusalLines(cliArgv); + if (cliRefusal) { + for (const line of cliRefusal) console.error(line); + process.exit(EXIT_USAGE); + } const rearmed = rearmThroughProxy(process.argv.slice(2)); if (rearmed !== null) process.exit(rearmed); main(process.argv.slice(2)).then((code) => process.exit(code));