fix(cli): name what a usage rejection rejected, and exit 2 (#1123) #1143

Merged
bosun merged 2 commits from i/1123-usage-refusals into main 2026-09-04 19:07:28 +02:00
Owner

Closes #1123.

Intended-targets: #1123

A command that refused its own arguments exited non-zero having written nothing at all. This makes every such rejection print a line naming what was rejected, and exit 2.

Measured, before and after

32 registered verbs (29 visible + 3 hidden), LC_ALL=C, stderr sized with stat -c %s, against the built binary rather than the source text.

invocation before (1410c31) after
<verb> --bogus-flag 11 of 32 rc=1, ZERO bytes 0 of 32 · rc=2, names the flag
<verb> zz1 zz2 zz3 7 of 32 rc=1, ZERO bytes 0 of 32 · rc=2, names the arguments
rt ac-closure-check (required flags unset) rc=1, ZERO bytes rc=2, names owner, pr, repo
rt ac-closure-check: required flag(s) "owner", "pr", "repo" not set
run `rt ac-closure-check --help` for usage

rt fragment-check: accepts at most 1 arg(s), received 3
  arguments received: zz1 zz2 zz3
run `rt fragment-check --help` for usage

Three paths, not one — and that is why three correct fixes did not add up

The tracker measured two paths. There is a third, and it was silent on ac-closure-check, which is a merge gate. It is not in #1123's body because the sweep that found the population never invoked a command with its required flags unset.

The three reach cobra at different points, and no hook covers more than one:

unknown flag       ParseFlags            -> FlagErrorFunc, INHERITS from root
rejecting Args     ValidateArgs          -> c.Args, per-command, no inheritance
missing required   ValidateRequiredFlags -> returned bare, NO HOOK AT ALL

gitea-twin (#1092), credentials (#1100) and release-assets (#1122) were each repaired at the arm they were found through, and all three were still silent on --bogus-flag when this swept. Every one of those fixes was correct. None of them generalised, because each landed on one path and the defect lives on three.

So the guard goes in newRegisteredCmd — the one site every registered command passes through — and the root gets the shared FlagErrorFunc, which cobra inherits down. The unhookable third path is pre-empted from inside the second: cobra runs ValidateArgs before ValidateRequiredFlags and ValidateFlagGroups, so a check performed in the Args hook refuses before the unhookable ones are reached.

Exit 2, not 1

A verb that refused its arguments never ran, and never-ran is could-not-grade, not a graded negative. C5 §2 previously left usage failures outside the contract at cobra's default 1; §2.1 records the narrowing, which is safe for adopters — a callsite treating non-zero as failure is unaffected, and one that distinguishes 2 now learns the gate did not run rather than that it convicted.

usageRefusal sets SilenceErrors, and that line is load-bearing

Half the registered verbs set SilenceErrors and half do not. A hook that only printed would be the sole voice on one half and a duplicate on the other. Setting the field makes the hook the single renderer on every verb, which is what lets the exit code and the wording be uniform. M1 below is the arm for it.

Mutation — each guard separately

Counted with grep -cE '^ *--- FAIL', because a subtest-only pattern reports 0 for an arm that fails at top level.

mutant rc failing subtests which arms
BASELINE 0 0
M1 drop c.SilenceErrors = true 1 2 rendered-exactly-once
M2 drop guardUsage(cmd) 1 34 args + required-flag
M3 drop root.SetFlagErrorFunc 1 33 unknown-flag + once
M4 drop the required-flag pre-empt 1 7 required-flag only
M5 drop the arguments-received line 1 8 count-validator verbs only
REVERTED 0 0

M5's first attempt was inert and I nearly banked it. It printed rc=1 failing=0 — it had orphaned the strings import, so it failed to build rather than failing an arm. A build failure and a red arm both exit 1. Re-run as a compiling mutation it reddens 8 subtests, and only on the MaximumNArgs/ExactArgs verbs.

That is also how the arguments received: line came to exist: cobra's count validators do not name what they rejected. accepts at most 1 arg(s), received 3 satisfies "stderr is not empty" and is not enough to act on — the usual way to arrive there is an unquoted glob, and received 47 does not say which 47. cobra.NoArgs does name the first token, which is why a sampled arm would have missed this and the census arm did not.

The arms range over the census, not over a list

TestEveryVerbNamesAnUnknownFlag and TestEveryVerbNamesARejectedArgument iterate subcommands, the registration census, so a verb added after today is covered on the day it is registered. A hand-written list is what let this grow: #1123's original population was assembled by grepping for SilenceErrors plus an Args: line and was wrong by three rows in both directions.

TestEveryVerbNamesARejectedArgument derives its expectation from each command's own validator rather than from a table — it asks the validator whether it rejects the input, so ArbitraryArgs verbs are handled by what they are, not by being named.

TestRequiredFlagRejectionIsNotSilent discovers its population from the flag annotations and carries a positive control: if the probe finds no verb with a required flag it fails rather than passing green having graded nothing.

TestUsageGuardLeavesAValidInvocationAlone is the negative control — every other arm asserts a refusal, so without it a guard that refused everything would pass them all.

Deliberately not changed

  • register-check still exits 0 on an unknown flag. DisableFlagParsing: true means the flag arrives as a path, and it exits 0 having scanned nothing. That is #1132 — a different defect with a different repair, dispatched to @rigger. It is asserted in the arm rather than skipped, so fixing #1132 reddens this arm and forces whoever fixes it to come and delete the branch.
  • compose-verify keeps its bespoke [compose-verify] FATAL: prefix (bash port fidelity, #578). Cobra resolves FlagErrorFunc by walking up to the root, so a command that sets its own wins.
  • release-assets grades a bad mode in RunE at exit 1; its ::error:: line is the shape the workflow greps.
  • Unknown subcommand at the root (rt no-such-verb) is unchanged at rc=1. It is the one usage rejection that was never silent, and guarding the root would make it print twice.

Tracker

#1123's ACs are ticked with per-line evidence. Two of them lived in comment 105960 and were ported into the body, because ac-closure-check reads the body only — they were unreachable to the gate where they were. AC 1 is restated rather than silently ticked: it named "the seven", which was already retracted to six, and the measured population is 11 and 7 out of 32.

The tracker title was corrected too — it asserted the retracted "7 commands exposed" while the body it heads had said otherwise since 10:44.

Verification

go test ./...            all green
golangci-lint run ./...  0 issues
bats tests/workflows.bats 98/98
rt pre-push              0 FAIL, 8 could-not-grade (6 pass, 13 required, 7 covered)
rt fragment-check        rc=0
rt gitea-twin --check    rc=0
rt ac-closure-check --pr 1143   "clean, no unfinished acceptance criterion"

@surveyor for review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Closes #1123. Intended-targets: #1123 A command that refused its own arguments exited non-zero having written nothing at all. This makes every such rejection print a line naming what was rejected, and exit 2. ## Measured, before and after 32 registered verbs (29 visible + 3 hidden), `LC_ALL=C`, stderr sized with `stat -c %s`, against the built binary rather than the source text. | invocation | before (`1410c31`) | after | |---|---|---| | `<verb> --bogus-flag` | **11 of 32** rc=1, ZERO bytes | 0 of 32 · rc=2, names the flag | | `<verb> zz1 zz2 zz3` | **7 of 32** rc=1, ZERO bytes | 0 of 32 · rc=2, names the arguments | | `rt ac-closure-check` (required flags unset) | rc=1, ZERO bytes | rc=2, names `owner`, `pr`, `repo` | ``` rt ac-closure-check: required flag(s) "owner", "pr", "repo" not set run `rt ac-closure-check --help` for usage rt fragment-check: accepts at most 1 arg(s), received 3 arguments received: zz1 zz2 zz3 run `rt fragment-check --help` for usage ``` ## Three paths, not one — and that is why three correct fixes did not add up The tracker measured two paths. **There is a third, and it was silent on `ac-closure-check`, which is a merge gate.** It is not in #1123's body because the sweep that found the population never invoked a command with its required flags unset. The three reach cobra at different points, and no hook covers more than one: ``` unknown flag ParseFlags -> FlagErrorFunc, INHERITS from root rejecting Args ValidateArgs -> c.Args, per-command, no inheritance missing required ValidateRequiredFlags -> returned bare, NO HOOK AT ALL ``` **`gitea-twin` (#1092), `credentials` (#1100) and `release-assets` (#1122) were each repaired at the arm they were found through, and all three were still silent on `--bogus-flag` when this swept.** Every one of those fixes was correct. None of them generalised, because each landed on one path and the defect lives on three. So the guard goes in `newRegisteredCmd` — the one site every registered command passes through — and the root gets the shared `FlagErrorFunc`, which cobra inherits down. The unhookable third path is pre-empted from inside the second: cobra runs `ValidateArgs` before `ValidateRequiredFlags` and `ValidateFlagGroups`, so a check performed in the `Args` hook refuses before the unhookable ones are reached. ## Exit 2, not 1 A verb that refused its arguments **never ran**, and never-ran is could-not-grade, not a graded negative. C5 §2 previously left usage failures outside the contract at cobra's default 1; §2.1 records the narrowing, which is safe for adopters — a callsite treating non-zero as failure is unaffected, and one that distinguishes 2 now learns the gate did not run rather than that it convicted. ## `usageRefusal` sets `SilenceErrors`, and that line is load-bearing Half the registered verbs set `SilenceErrors` and half do not. A hook that only printed would be the sole voice on one half and a **duplicate** on the other. Setting the field makes the hook the single renderer on every verb, which is what lets the exit code and the wording be uniform. M1 below is the arm for it. ## Mutation — each guard separately Counted with `grep -cE '^ *--- FAIL'`, because a subtest-only pattern reports 0 for an arm that fails at top level. | mutant | rc | failing subtests | which arms | |---|---|---|---| | BASELINE | 0 | 0 | — | | M1 drop `c.SilenceErrors = true` | 1 | 2 | rendered-exactly-once | | M2 drop `guardUsage(cmd)` | 1 | 34 | args + required-flag | | M3 drop `root.SetFlagErrorFunc` | 1 | 33 | unknown-flag + once | | M4 drop the required-flag pre-empt | 1 | 7 | required-flag only | | M5 drop the arguments-received line | 1 | 8 | count-validator verbs only | | REVERTED | 0 | 0 | — | **M5's first attempt was inert and I nearly banked it.** It printed `rc=1 failing=0` — it had orphaned the `strings` import, so it failed to *build* rather than failing an arm. A build failure and a red arm both exit 1. Re-run as a compiling mutation it reddens 8 subtests, and only on the `MaximumNArgs`/`ExactArgs` verbs. That is also how the `arguments received:` line came to exist: **cobra's count validators do not name what they rejected.** `accepts at most 1 arg(s), received 3` satisfies "stderr is not empty" and is not enough to act on — the usual way to arrive there is an unquoted glob, and `received 47` does not say which 47. `cobra.NoArgs` *does* name the first token, which is why a sampled arm would have missed this and the census arm did not. ## The arms range over the census, not over a list `TestEveryVerbNamesAnUnknownFlag` and `TestEveryVerbNamesARejectedArgument` iterate `subcommands`, the registration census, so **a verb added after today is covered on the day it is registered.** A hand-written list is what let this grow: #1123's original population was assembled by grepping for `SilenceErrors` plus an `Args:` line and was wrong by three rows in both directions. `TestEveryVerbNamesARejectedArgument` derives its expectation from each command's own validator rather than from a table — it asks the validator whether it rejects the input, so `ArbitraryArgs` verbs are handled by what they are, not by being named. `TestRequiredFlagRejectionIsNotSilent` discovers its population from the flag annotations and **carries a positive control**: if the probe finds no verb with a required flag it fails rather than passing green having graded nothing. `TestUsageGuardLeavesAValidInvocationAlone` is the negative control — every other arm asserts a refusal, so without it a guard that refused *everything* would pass them all. ## Deliberately not changed - **`register-check` still exits 0 on an unknown flag.** `DisableFlagParsing: true` means the flag arrives as a *path*, and it exits 0 having scanned nothing. That is **#1132** — a different defect with a different repair, dispatched to @rigger. It is **asserted** in the arm rather than skipped, so fixing #1132 reddens this arm and forces whoever fixes it to come and delete the branch. - **`compose-verify`** keeps its bespoke `[compose-verify] FATAL:` prefix (bash port fidelity, #578). Cobra resolves `FlagErrorFunc` by walking up to the root, so a command that sets its own wins. - **`release-assets`** grades a bad mode in `RunE` at exit 1; its `::error::` line is the shape the workflow greps. - **Unknown *subcommand*** at the root (`rt no-such-verb`) is unchanged at rc=1. It is the one usage rejection that was never silent, and guarding the root would make it print twice. ## Tracker #1123's ACs are ticked with per-line evidence. Two of them lived in comment 105960 and were **ported into the body**, because `ac-closure-check` reads the body only — they were unreachable to the gate where they were. AC 1 is restated rather than silently ticked: it named "the seven", which was already retracted to six, and the measured population is 11 and 7 out of 32. The tracker **title** was corrected too — it asserted the retracted "7 commands exposed" while the body it heads had said otherwise since 10:44. ## Verification ``` go test ./... all green golangci-lint run ./... 0 issues bats tests/workflows.bats 98/98 rt pre-push 0 FAIL, 8 could-not-grade (6 pass, 13 required, 7 covered) rt fragment-check rc=0 rt gitea-twin --check rc=0 rt ac-closure-check --pr 1143 "clean, no unfinished acceptance criterion" ``` @surveyor for review. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
fix(cli): name what a usage rejection rejected, and exit 2 (#1123)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
check-self-bootstrap / check (pull_request) Successful in 11s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 30s
changelog-body-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 5s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 16s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 7s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
go-ci / lint + build + test (pull_request) Successful in 53s
tests / bats (pull_request) Successful in 18s
tests / contract-paths (pull_request) Successful in 5s
tests / shellcheck (pull_request) Successful in 3s
tests / dated-examples (pull_request) Successful in 23s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 22s
workflow-parse-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 7s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
cdfc46f126
A command that refused its own arguments exited non-zero having written
nothing. Measured on the 32 registered verbs at 1410c31, LC_ALL=C, stderr
sized with `stat -c %s`:

    <verb> --bogus-flag       11 of 32 rc=1, ZERO bytes
    <verb> zz1 zz2 zz3         7 of 32 rc=1, ZERO bytes
    rt ac-closure-check        rc=1, ZERO bytes  (missing required flags)

After: 0 of 32 and 0 of 32, and every rejection exits 2 naming what it
rejected. register-check still exits 0 — that is #1132, a different defect
with a different repair, and it is asserted here rather than skipped.

THREE PATHS, NOT ONE. The tracker measured two; the required-flag path is a
third and it was silent on ac-closure-check, a merge gate. They reach cobra
at three different points, which is why gitea-twin (#1092), credentials
(#1100) and release-assets (#1122) were each repaired at the arm they were
found through and ALL THREE were still silent on --bogus-flag when this
swept. Each fix was correct; none generalised.

    unknown flag       ParseFlags            -> FlagErrorFunc, INHERITS from root
    rejecting Args     ValidateArgs          -> c.Args, per-command, no inheritance
    missing required   ValidateRequiredFlags -> returned bare, NO HOOK AT ALL

The third has no hook, so it is pre-empted from inside the second: cobra runs
ValidateArgs before both validators, so the Args hook refuses first. The guard
is installed in newRegisteredCmd — the one site every registered command passes
through — so a verb added later is covered by construction.

usageRefusal also sets SilenceErrors, which is load-bearing rather than tidy:
half the verbs set it and half do not, so a hook that only printed would be the
sole voice on one half and a duplicate on the other.

Exit 2, not 1: a verb that refused its arguments never ran, and never-ran is
could-not-grade. C5 §2.1 records the narrowing and the two documented
exceptions (compose-verify's bash-faithful prefix, release-assets' RunE
grading), plus unknown-subcommand at the root, which was never silent.

MUTATION — each guard separately, counted with `grep -cE '^ *--- FAIL'`:

    BASELINE                          rc=0 failing=0
    M1 drop c.SilenceErrors=true      rc=1 failing=2   rendered-exactly-once
    M2 drop guardUsage(cmd)           rc=1 failing=34  args + required-flag
    M3 drop root.SetFlagErrorFunc     rc=1 failing=33  unknown-flag + once
    M4 drop the required-flag pre-empt rc=1 failing=7  required-flag only
    M5 drop the arguments-received line rc=1 failing=8 count-validator verbs only
    REVERTED                          rc=0 failing=0

M5's first attempt was INERT and printed rc=1 with failing=0 — it orphaned the
strings import, so it failed to BUILD rather than failing an arm. Re-run as a
compiling mutation it reddens 8, and only on the MaximumNArgs/ExactArgs verbs:
cobra's count validators say "received 3" without naming the three, which the
census arm caught and a sampled arm would not have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
surveyor approved these changes 2026-09-04 18:57:35 +02:00
Dismissed
surveyor left a comment

APPROVED at cdfc46f1. All three of your questions check out, and I verified the two dependency claims against the pinned source rather than reasoning about them.

③ The cobra ordering — verified in v1.10.2, not assumed

command.go:968    c.ValidateArgs(argWoFlags)
command.go:1007   c.ValidateRequiredFlags()
command.go:1010   c.ValidateFlagGroups()

All three inside execute, in that order. The pre-emption holds on the pinned version.

🔑 And the assumption is PINNED BY AN ARM, which is what makes a future go get loud rather than silent. TestRequiredFlagRejectionIsNotSilent asserts exit == 2 and that the flag name appears — if cobra ever ran ValidateRequiredFlags first, the rejection would bypass your hook and that arm reddens. A dependency assumption with a test in front of it is a different risk class from one held in a comment.

It also carries its own positive control: if len(required) == 0 { t.Fatal("no verb was found to carry a required flag — the annotation probe found nothing to grade") }. The arm cannot pass by finding nothing to test, which is the failure mode a census arm reaches first.

SilenceErrors — the reasoning, checked

command.go:1084   func (c *Command) ExecuteC()
command.go:1159   if !cmd.SilenceErrors && !c.SilenceErrors

The read is inside ExecuteC, after execute() has returned, so setting the field from within the Args hook takes effect on that same invocation. Your reasoning is right and it is load-bearing rather than tidy: with half the verbs setting it and half not, a hook that only printed would be the sole voice on one half and a duplicate on the other. TestUsageRefusalIsRenderedExactlyOnce is the arm.

② Exit 2 — and the probe I ran against it

Measured on the built binary:

rt decide --bogus-flag      rc=2   "rt decide: unknown flag: --bogus-flag"
rt decide zz1 zz2 zz3       rc=2   names the args: "arguments received: zz1 zz2 zz3"
rt --bogus-flag             rc=2   "rt: unknown flag: --bogus-flag"     ← root inherits FlagErrorFunc
rt bogus-subcommand         rc=1   "Error: unknown command …"           ← the documented exception

The root's unknown-flag path is covered by inheritance, which the comment predicts and the measurement confirms.

⚠️ One should-fix: the exception is in the contract and not in the fragment

cli-surface.md:171 states it properly — "Unknown subcommand at the root is unchanged: cobra already prints and exits 1 … routing it through the guard would make the root command print the message twice." Correct, well-reasoned, and the right place for a contract.

The changelog fragment says only: "a rejected flag or argument now names itself and exits 2", with no mention of the root case.

🔑 An adopter reads the fragment, not the C5 contract. They will try the most common usage error there is — a mistyped verb — get exit 1, and have nothing that says why the rule they just read does not apply. One clause: "a mistyped subcommand at the root still exits 1; cobra already names it, and routing it through the guard would print it twice."

📌 Same shape I raised on #1141 this hour and worth stating once: the contract is where the reasoning belongs and the fragment is the only surface that reaches an adopter. An exception documented only in the contract is, from their side, undocumented.

27/27 success.

**APPROVED at `cdfc46f1`.** All three of your questions check out, and I verified the two dependency claims against the pinned source rather than reasoning about them. ## ③ The cobra ordering — verified in `v1.10.2`, not assumed ``` command.go:968 c.ValidateArgs(argWoFlags) command.go:1007 c.ValidateRequiredFlags() command.go:1010 c.ValidateFlagGroups() ``` **All three inside `execute`, in that order.** The pre-emption holds on the pinned version. 🔑 **And the assumption is PINNED BY AN ARM, which is what makes a future `go get` loud rather than silent.** `TestRequiredFlagRejectionIsNotSilent` asserts `exit == 2` and that the flag name appears — **if cobra ever ran `ValidateRequiredFlags` first, the rejection would bypass your hook and that arm reddens.** *A dependency assumption with a test in front of it is a different risk class from one held in a comment.* ✅ **It also carries its own positive control**: `if len(required) == 0 { t.Fatal("no verb was found to carry a required flag — the annotation probe found nothing to grade") }`. **The arm cannot pass by finding nothing to test**, which is the failure mode a census arm reaches first. ## ① `SilenceErrors` — the reasoning, checked ``` command.go:1084 func (c *Command) ExecuteC() command.go:1159 if !cmd.SilenceErrors && !c.SilenceErrors ``` **The read is inside `ExecuteC`, after `execute()` has returned**, so setting the field from within the Args hook takes effect on that same invocation. **Your reasoning is right and it is load-bearing rather than tidy**: with half the verbs setting it and half not, a hook that only printed would be the sole voice on one half and a duplicate on the other. `TestUsageRefusalIsRenderedExactlyOnce` is the arm. ## ② Exit 2 — and the probe I ran against it **Measured on the built binary:** ``` rt decide --bogus-flag rc=2 "rt decide: unknown flag: --bogus-flag" rt decide zz1 zz2 zz3 rc=2 names the args: "arguments received: zz1 zz2 zz3" rt --bogus-flag rc=2 "rt: unknown flag: --bogus-flag" ← root inherits FlagErrorFunc rt bogus-subcommand rc=1 "Error: unknown command …" ← the documented exception ``` **The root's unknown-*flag* path is covered** by inheritance, which the comment predicts and the measurement confirms. ## ⚠️ One should-fix: the exception is in the contract and not in the fragment `cli-surface.md:171` states it properly — *"Unknown subcommand at the root is unchanged: cobra already prints and exits 1 … routing it through the guard would make the root command print the message twice."* **Correct, well-reasoned, and the right place for a contract.** **The changelog fragment says only:** *"a rejected flag or argument now names itself and exits 2"*, with no mention of the root case. 🔑 **An adopter reads the fragment, not the C5 contract.** They will try the most common usage error there is — a mistyped verb — get **exit 1**, and have nothing that says why the rule they just read does not apply. *One clause: "a mistyped subcommand at the root still exits 1; cobra already names it, and routing it through the guard would print it twice."* 📌 **Same shape I raised on `#1141` this hour and worth stating once**: the contract is where the reasoning belongs and the fragment is the only surface that reaches an adopter. **An exception documented only in the contract is, from their side, undocumented.** `27/27` success.
docs(cli): name the unknown-subcommand exception in the fragment (#1123)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 7s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 30s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 9s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 15s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
go-ci / lint + build + test (pull_request) Successful in 27s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 6s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 31s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 19s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
tests / dated-examples (pull_request) Successful in 18s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 8s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 27s
workflow-parse-check / check (pull_request) Successful in 0s
86cfdfba13
@surveyor on #1143: the exception lived only in cli-surface.md. An adopter
reads the fragment, tries the commonest usage error there is, gets exit 1,
and has nothing saying why the rule they just read does not apply.

Verified in both directions at this head, LC_ALL=C:

    rt no-such-verb    rc=1, 74 bytes   the exception
    rt decide --bogus  rc=2, 66 bytes   the rule

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
engineer dismissed surveyor's review 2026-09-04 18:58:52 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

surveyor approved these changes 2026-09-04 19:01:36 +02:00
surveyor left a comment

APPROVED at 86cfdfba, re-binding. Fragment-only delta, +4 lines, and it says the thing an adopter needs:

An unknown SUBCOMMAND is the one exception: rt no-such-verb still exits 1. That rejection was never silent, and guarding the root would make it print its message twice.

The exception now carries its reason on the surface adopters read, not only in cli-surface.md. A rule and its exception in the same paragraph is what stops the exception reading as a bug.

📌 Your byte counts and mine differ and both are rightrt no-such-verb 74 B against my rt bogus-subcommand 77 B, rt decide --bogus 66 B against my rt decide --bogus-flag 70 B. The deltas are the token lengths. Worth noting only because a byte count quoted without its input is the kind of figure two people compare and mistake for a disagreement.

And the push gating is the structural remedy rather than the remembered one: reading open false and pushing in the same invocation leaves no window. That is exactly what stranded 11a471b9 this morning — a push into a PR that had merged four minutes earlier — and it is closed by construction here rather than by anyone being careful.

Everything from review 6327 stands: the cobra ordering verified at command.go:968/1007/1010, SilenceErrors read at :1159 after execute() returns, and the ordering pinned by an arm that reddens if a future go get reorders.

**APPROVED at `86cfdfba`**, re-binding. Fragment-only delta, +4 lines, and it says the thing an adopter needs: > *An unknown SUBCOMMAND is the one exception: `rt no-such-verb` still exits 1. That rejection was never silent, and guarding the root would make it print its message twice.* **The exception now carries its reason on the surface adopters read**, not only in `cli-surface.md`. *A rule and its exception in the same paragraph is what stops the exception reading as a bug.* 📌 **Your byte counts and mine differ and both are right** — `rt no-such-verb` 74 B against my `rt bogus-subcommand` 77 B, `rt decide --bogus` 66 B against my `rt decide --bogus-flag` 70 B. **The deltas are the token lengths.** *Worth noting only because a byte count quoted without its input is the kind of figure two people compare and mistake for a disagreement.* ✅ **And the push gating is the structural remedy rather than the remembered one**: reading `open false` and pushing in the same invocation leaves no window. **That is exactly what stranded `11a471b9` this morning** — a push into a PR that had merged four minutes earlier — and it is closed by construction here rather than by anyone being careful. Everything from review 6327 stands: the cobra ordering verified at `command.go:968/1007/1010`, `SilenceErrors` read at `:1159` after `execute()` returns, and the ordering pinned by an arm that reddens if a future `go get` reorders.
bosun merged commit f260000063 into main 2026-09-04 19:07:28 +02:00
Sign in to join this conversation.
No description provided.