Skip to content

Commit 2ee88cb

Browse files
os-litantclaude
andauthored
fix(pm): refuse --tier against --commands/--json instead of silently preferring the tier (#14765)
`--tier` is a third shape of stdout and was the one the "two spellings of stdout" rule did not name. `mode` was computed from --commands/--json and then discarded by the `--tier` branch, so the tier prose landed in a stream whose caption promises one runnable command per line, or one JSON document — at exit 0, with nothing on either stream saying the flag had been dropped. Both pairs now refuse with the shape the existing refusals use: exit 2, one `dispatch-gates: …` line on stderr naming both flags. Ordered after the `--commands --json` pair so all three together keep the message they had. `--tier` on its own is byte-identical to before. Fixes #14294 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1596b4c commit 2ee88cb

1 file changed

Lines changed: 48 additions & 2 deletions

File tree

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17407,6 +17407,25 @@ function selfTest() {
1740717407
// Two answers to "what shape is stdout" is no answer.
1740817408
const bothRun = runCli(['--commands', '--json', seamCard]);
1740917409
t('passing both stdout spellings refuses instead of silently preferring one', bothRun.status === 2 && (bothRun.stdout ?? '').trim() === '');
17410+
// ⭐ The THIRD stdout shape (#14294). The pair above was enforced and this
17411+
// one was not: `mode` was computed and then discarded, so these two exited
17412+
// 0 with tier prose sitting in the file a consumer had redirected because
17413+
// `--commands` promises commands and nothing else. Driven on the real CLI
17414+
// for the same reason the pair above is — the defect was in the argv chain,
17415+
// which no pure half reaches. Both halves of the refusal are asserted: the
17416+
// STATUS, and the stdout a redirecting consumer would have kept, because a
17417+
// refusal that still prints an answer is the bug wearing an exit code.
17418+
const tierCmdRun = runCli(['--tier', '--commands', seamCard]);
17419+
t('⭐ --tier against --commands refuses instead of silently preferring the tier', tierCmdRun.status === 2 && (tierCmdRun.stdout ?? '').trim() === '');
17420+
t('and its refusal names BOTH flags, so the dropped one is never left to be guessed', (tierCmdRun.stderr ?? '').includes('--tier') && (tierCmdRun.stderr ?? '').includes('--commands'));
17421+
const tierJsonRun = runCli(['--tier', '--json', seamCard]);
17422+
t('⭐ --tier against --json refuses the same way, on the other machine-readable shape', tierJsonRun.status === 2 && (tierJsonRun.stdout ?? '').trim() === '');
17423+
t('and that refusal names --json, never the flag this run is not about', (tierJsonRun.stderr ?? '').includes('--json') && !(tierJsonRun.stderr ?? '').includes('--commands'));
17424+
// CONTROL: green before this fix and after it. The new branch is ordered
17425+
// after the pair, so all three flags together still answer with the pair's
17426+
// own message — the fix adds a refusal and rewords none.
17427+
const allThreeRun = runCli(['--tier', '--commands', '--json', seamCard]);
17428+
t('CONTROL: all three together keep the pair rule that was already enforced', allThreeRun.status === 2 && (allThreeRun.stderr ?? '').includes('--commands and --json'));
1741017429
}
1741117430

1741217431
// ── END TO END: the CI-measured family, on the card it was measured on (#14004)
@@ -17792,9 +17811,36 @@ if (invokedDirectly) {
1779217811
// Two answers to "what shape is stdout" is no answer. Blending them — or
1779317812
// silently preferring one — is the class of failure this whole file is
1779417813
// about, and it would be a poor place to commit it: these two flags exist
17795-
// because a consumer could not tell what it was reading.
17814+
// because a consumer could not tell what it was reading. The rule is about
17815+
// how many answers the stream carries, not about which two flags happened
17816+
// to be named when it was written: `--tier` is a THIRD shape of that same
17817+
// stdout, and it is refused against each of these two in the branch below.
1779617818
console.error('dispatch-gates: --commands and --json are two spellings of stdout — pass one.');
1779717819
process.exit(2);
17820+
} else if (process.argv.includes('--tier') && (process.argv.includes('--commands') || process.argv.includes('--json'))) {
17821+
// The third shape, and the one the pair above did not reach: `mode` was
17822+
// computed from --commands/--json and then DISCARDED by the `--tier` branch
17823+
// at the bottom of this block, so the tier PROSE landed in a stream whose
17824+
// caption promises one runnable command per line, or one JSON document —
17825+
// at exit 0, with nothing on either stream saying the flag had been
17826+
// dropped. That is the silent preference the rule above forbids, committed
17827+
// one flag over from where it is enforced.
17828+
//
17829+
// REFUSED, not re-routed. Sending the tier verdict to stderr whenever a
17830+
// machine-readable mode is asked for is defensible — `--tier` answers a
17831+
// different question and reads no tree — but it changes what `--tier`
17832+
// MEANS when combined, and that is a design call for this file's owner,
17833+
// not a repair to a silent-drop bug. Same shape as the two refusals above
17834+
// it and the `--ran --tier` refusal above them: one line, exit 2, both
17835+
// flags named so the caller never has to guess which one was dropped.
17836+
//
17837+
// Ordered AFTER the pair deliberately: `--tier --commands --json` keeps
17838+
// the message it already had, so this branch adds a refusal and changes
17839+
// none.
17840+
console.error(
17841+
`dispatch-gates: --tier and ${process.argv.includes('--commands') ? '--commands' : '--json'} are two spellings of stdout — pass one.`,
17842+
);
17843+
process.exit(2);
1779817844
} else if (wantsChanged && argvPaths.length > 0) {
1779917845
// The two input modes answer different questions and must never be blended:
1780017846
// silently preferring one would make the other's arguments vanish without a
@@ -17862,7 +17908,7 @@ if (invokedDirectly) {
1786217908
derived = changedPathsFromGit();
1786317909
} catch (err) {
1786417910
console.error(`dispatch-gates: could not derive the change set — ${err.message}`);
17865-
console.error('usage: node scripts/pm/dispatch-gates.mjs [--residue] [--tier] [--commands | --json | --ran <file>] [--repo owner/name] [<path> ...] | --changed | --self-test');
17911+
console.error('usage: node scripts/pm/dispatch-gates.mjs [--residue] [--tier | --commands | --json | --ran <file>] [--repo owner/name] [<path> ...] | --changed | --self-test');
1786617912
process.exit(2);
1786717913
}
1786817914
if (derived.paths.length === 0) {

0 commit comments

Comments
 (0)