fix(cli): stop commands printing their own exit code as "Error: 1" (#1113) - #1134
Conversation
…1113) typer.Exit subclasses RuntimeError, so a command's catch-all caught its own deliberate exit and stringified the exit *code* as a message: Error: No PRD found. Add one first: codeframe prd add <file.md> Error: 1 To a new user that reads as a second, unexplained failure immediately after a message that was otherwise clear and actionable. Five commands were structurally vulnerable — tasks_generate, templates_apply, schedule_show, schedule_predict, schedule_bottlenecks — each now re-raising typer.Exit ahead of its catch-all, the guard prd_generate already had. The issue lists 11 commands; an AST scan finds 5, and only 2 reproduce with the inputs available on an empty workspace (the other 3 are latent — their error paths need different conditions). Rather than argue the count, the scanner is now the rule: tests/cli/test_typer_exit_not_swallowed_1113.py fails for ANY function that raises typer.Exit inside a try whose broad handler would catch it, which is the enforcement the issue asked for in preference to spot fixes. It names the offending function and line. The scanner is verified non-tautological two ways: a unit test proving it detects the pattern in a synthetic sample, and by removing a real guard, which fails three tests including the user-visible output check. Verified on a real workspace: `cf tasks generate` with no PRD now prints its message and nothing else, still exiting 1.
|
Claude finished @frankbria's task in 4m 9s —— View job Bug-hunting review complete
Result: no concrete defects found. Summary posted to the PR. What I verified against the checked-out code:
|
ReviewSolid, well-scoped fix. The root cause analysis is correct ( Finding: the scanner doesn't check handler order
guarded = any(
isinstance(name, ast.Attribute) and name.attr == "Exit"
for handler in node.handlers
for name in _handler_names(handler)
)It never checks that this handler appears before the broad one. Python matches Relatedly, the check only confirms a Minor:
|
|
✅ GLM review: no defects found. Checked against the checked-out code: the 5 |
Closes #1113.
Before / after
Both captured from a real workspace.
Cause
typer.ExitsubclassesRuntimeError, soexcept Exception as ecatches acommand's own deliberate exit and prints
e— which stringifies to the exitcode.
prd_generatealready had theexcept typer.Exit: raiseguard; theothers did not.
On the count: the issue says 11, I found 5
An AST scan of
app.pyfinds 5 structurally vulnerable commands —tasks_generate,templates_apply,schedule_show,schedule_predict,schedule_bottlenecks. Running every command the issue names against an emptyworkspace, only 2 actually reproduce; the other 3 are latent because their
error paths need conditions an empty workspace does not produce, and the rest
either exit 0 or already guard correctly.
I did not try to reconcile the number, because the fix that matters is not a
list of commands.
The scanner is the rule
tests/cli/test_typer_exit_not_swallowed_1113.pyfails for any function thatraises
typer.Exitinside atrywhose broad handler would catch it, naming thefunction and line:
It deliberately inspects only the try body — a
raiseinside a handlerpropagates out of the statement rather than into a sibling handler, so counting
those would produce false positives.
This is what the issue asked for ("preferably a shared helper or a lint rule, so
a new command cannot reintroduce the pattern"). A per-command fix list would go
stale the next time someone adds a command.
Verified non-tautological, two ways
scanner that cannot fail is worse than no scanner.
output check, not just the scanner.
Acceptance criteria
typer.Exitbefore the catch-allcf tasks generatewith no PRD prints its message and nothing elseError: 1is absent and exit code is 1ruffclean;tests/cli/: 552 passed.