fix: usage errors exit 3, disjoint from the honest lane - #30
Merged
Merged
Conversation
FAMILY.md clause 1: exit 2 is the honest lane -- a first-class cannot-adjudicate verdict a consuming agent routes to a human WITHOUT retrying. A malformed invocation is the opposite: the caller should fix the call and retry. Sharing one code made the two indistinguishable, so an agent following the docs escalated its own bad calls to a human as if they were rulings. Exit 3 is the family precedent, set by srdcheck. ConfigError was doing double duty: unknown commands and flags raised it, but so did legitimate refusals that correctly exit 2. Added UsageError (a ConfigError subclass, caught first) so only malformed calls move. Verified `init` on an existing file still exits 2. SKILL.md and tool.json updated to match -- a code change that leaves the contract prose stale is the exact defect this whole effort exists to catch. Regression tests assert usage errors exit 3 AND that legitimate refusals stay on 2, because the risk here is over-applying the change. Also bumps the family-conformance srdcheck pin to the first baseline that no longer waives HONEST_LANE_OVERLOAD; both must land together or the ratchet fails with STALE BASELINE. Closes #1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_unknown_command_refuses and the unknown/duplicate/missing-flag cases asserted exit 2. They were right for the old contract; the contract changed. Also moves duplicate-flag and flag-missing-its-value onto UsageError. Those are malformed calls in exactly the same sense as an unknown flag, and leaving them on 2 would have made the separation arbitrary. Verified the boundary holds: init-on-existing-file, consumed-id-not-open, report-insufficient-data and the symlink-ledger refusal all still exit 2. Those are verdicts, not bad calls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1's exit-semantics half. This makes table-kit fully conformant — the first member other than srdcheck to reach
PASS.The defect
FAMILY.md clause 1: exit 2 is the honest lane — a cannot-adjudicate answer a consuming agent routes to a human without retrying. A malformed call is the opposite. table-kit returned 2 for both.
The fix, and why it needed care
ConfigErrorwas doing double duty. Unknown commands and flags raised it — but so do legitimate refusals that correctly exit 2 (init: file exists;consumed: id not open). A blanket change would have moved those too and broken the honest lane instead of fixing it.Added
UsageError, aConfigErrorsubclass caught first inmain(), so only malformed calls move:init(fresh)qcfindingsinit(file exists) — a real refusalreport --zzz-bad-optionDocs
SKILL.mdandtool.jsonupdated — and while there, the exit-2 lane is now documented honestly. It previously read "can't do that" with usage-error examples only, omitting the incomplete-coverage lane thatqcandreportactually emit (detector.py:614-619,report.py:496-503). That omission was the other half of #1.New tests assert usage errors exit 3 and that
initon an existing file stays on 2, because over-applying this change was the real risk.Pin bump
Bundled, and required: chaoz23/srdcheck#88 removed this waiver, and the ratchet fails a waiver that no longer fires.
After this, table-kit has no waivers at all. Remaining issues (#24, #25, #26) are hygiene the gate doesn't probe.
🤖 Generated with Claude Code