Skip to content

fix(kanban): propagate subcommand exit codes from CLI dispatch - #85

Merged
exiao merged 2 commits into
live-configfrom
fix/kanban-cli-exit-codes
Jul 2, 2026
Merged

fix(kanban): propagate subcommand exit codes from CLI dispatch#85
exiao merged 2 commits into
live-configfrom
fix/kanban-cli-exit-codes

Conversation

@exiao

@exiao exiao commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Problem

hermes kanban <subcommand> always exited 0, even on handled failures. The argparse dispatch in hermes_cli/main.py called args.func(args) and discarded the return value. cmd_kanbankanban_command returns 1 (e.g. ValueError on an unknown task) or 2, but main() threw the code away, so the process still exited 0.

Repro (pre-fix):

$ hermes kanban comment --author card-drop -- t_nonexistent "x"
kanban: unknown task t_nonexistent
$ echo $?
0        # should be 1

Impact: the kanban card-drop receiver (PR #84) maps unknown-card → 404 only when proc.returncode != 0. Because the CLI exited 0, an inline follow-up posted to a bogus card_id returned a false 200 {"commented":true} while the comment actually no-op'd. Found during the Diligence E2E boot.

Fix

Class-level, single site — the general dispatch in main():

rc = args.func(args)
if isinstance(rc, int):
    sys.exit(rc)
  • The isinstance(rc, int) guard leaves handlers returning None on the implicit exit-0 path (no behavior change for them).
  • Verified no func-registered handler returns a bool or an int-as-data — only cmd_kanban, cmd_project, and cmd_whatsapp_cloud return ints, all shell exit codes. So this also fixes hermes project.
  • Mirrors the existing pattern (cmd_security already does sys.exit(int(code or 0))).

Tests

New tests/hermes_cli/test_kanban_cli_exit_code.py runs the real hermes kanban as a subprocess against a throwaway HERMES_HOME (an in-process handler call would bypass the discarding bug). Asserts unknown task → non-zero, unknown board → non-zero, successful list → 0.

Ground-truth: 2 fail pre-fix, all 3 pass post-fix.

$ python -m pytest tests/hermes_cli/test_kanban_cli_exit_code.py -q
3 passed
$ python -m pytest tests/hermes_cli/test_kanban_cli.py tests/hermes_cli/test_kanban_cli_dispatch_passthrough.py -q
51 passed
$ ruff check hermes_cli/main.py tests/hermes_cli/test_kanban_cli_exit_code.py
All checks passed!

Scope note

The receiver-side integration test (real hermes kanban comment → 404) belongs on the #84 branch (feat/kanban-card-drop-receiver, unmerged), not on live-config — coupling two unmerged branches would be wrong. Once #84 lands it can drop its returncode=1 stub for a real-CLI assertion; this core fix is what makes that possible.

Patch note: ~/.hermes/plans/hermes-patches/fix-kanban-cli-exit-codes.md

hermes_cli/main.py dispatch called args.func(args) and discarded the
return value, so hermes kanban <subcmd> always exited 0 even on handled
failures (unknown task/board -> kanban_command returns 1/2). That broke
the card-drop receiver's unknown-card -> 404 mapping, which keys on
proc.returncode != 0, producing a false 200 for a bogus card_id.

Propagate an int return via sys.exit at the single dispatch site;
isinstance guard leaves handlers returning None on the implicit exit-0
path. Fixes both kanban and project (both return exit codes).

Patch note: ~/.hermes/plans/hermes-patches/fix-kanban-cli-exit-codes.md
@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown

Claude finished @exiao's task in 1m 15s —— View job


PR Review: propagate subcommand exit codes

  • Read the dispatch change in hermes_cli/main.py
  • Verify the bool-subclass concern (gemini/codex) is resolved
  • Audit func-registered handlers for int-as-data returns
  • Review the new subprocess test

LGTM. Minimal, correctly-layered fix. Propagating the handler return value as a shell exit code at the single general dispatch site is the right place, and the guard is now correct.

Bool concern is resolved. The updated guard isinstance(rc, int) and not isinstance(rc, bool) (main.py:13638) correctly excludes bools, so a plugin CLI handler (registered via PluginContext.register_cli_command) returning True/False as a success flag keeps the implicit exit-0 path rather than mapping sys.exit(True) → 1. The parametrized test_bool_return_does_not_propagate_as_exit_code exercises this through the real main() dispatch for both True and False.

Handler audit confirms safety. Every func-registered handler returns either None or a genuine shell exit code — none returns an int-as-data (e.g. a count) that would be misinterpreted:

  • cmd_kanban, cmd_project, cmd_whatsapp_cloud → exit codes (the reported cases)
  • cmd_migrate / cmd_migrate_xaiint exit codes (0/2) — this PR now correctly propagates these too, a latent bonus fix
  • cmd_chat, cmd_moa, cmd_fallback, bundles_command, cmd_sessions, cmd_completion, _dispatch_secrets, cmd_computer_useNone

Notes (non-blocking):

  • The dispatch is the final statement in main(), so sys.exit() skips no cleanup — safe.
  • The ty report's new unresolved-import: pytest is an environment quirk in the type checker (pytest isn't on its resolution path) and, per the report, never fails the build — not a code issue.
  • The bool-return test monkeypatches m.cmd_kanban before calling main(), and set_defaults(func=cmd_kanban) resolves the name at parser-build time (inside main()), so the patched lambda is correctly picked up — the test is valid.

No prompt-cache, role-alternation, narrow-waist, env-var, or profile-safety concerns — this is pure CLI process-exit plumbing.

0 blocking, 0 minor.
· fix/kanban-cli-exit-codes

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🔎 Lint report: fix/kanban-cli-exit-codes vs origin/live-config

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11774 on HEAD, 11773 on base (🆕 +1)

🆕 New issues (1):

Rule Count
unresolved-import 1
First entries
tests/hermes_cli/test_kanban_cli_exit_code.py:18: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues: none

Unchanged: 6190 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request ensures that subcommand shell-style exit codes are correctly propagated by checking if the handler returns an integer and calling sys.exit(rc). It also adds comprehensive regression tests to verify this behavior. The review feedback correctly points out that because bool is a subclass of int in Python, isinstance(rc, int) will evaluate to True for boolean return values, which could lead to unexpected exit codes (e.g., sys.exit(True) exiting with status code 1). It is recommended to use type(rc) is int instead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread hermes_cli/main.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0fb9ca7cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hermes_cli/main.py Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — minimal, correct fix. Propagating the handler return value as a shell exit code at the single general dispatch site is the right layer, and the isinstance(rc, int) guard preserves the implicit exit-0 path for None-returning handlers. Audited every func-registered handler (bundled + plugin): none return a bool (the isinstance(True, int) gotcha) or an int-as-data — they return None or genuine exit codes, so the change is safe and even propagates codes for more commands than the PR body lists. The subprocess test correctly exercises the real main() exit path. One non-blocking note: this also broadens the contract for third-party plugin CLI handlers that might return True on success. 0 blocking.

bool is a subclass of int, so a handler returning a success/failure flag
(e.g. a plugin CLI command via PluginContext.register_cli_command) would be
treated as a shell exit code -- sys.exit(True) exits 1, inverting the signal.
Guard the dispatch with `not isinstance(rc, bool)` so only genuine int exit
codes propagate; bools and None keep the implicit exit-0 path.

Addresses gemini-code-assist and Codex P2 review findings on #85.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — minimal, correctly-layered fix. Propagating the handler return as a shell exit code at the single general dispatch site is right, and the updated guard isinstance(rc, int) and not isinstance(rc, bool) resolves the bool-subclass concern so plugin handlers returning success/failure flags keep the implicit exit-0 path. Audited every func-registered handler: all return None or genuine exit codes (cmd_migrate/cmd_migrate_xai are now correctly propagated too as a bonus), none return int-as-data. The parametrized subprocess test exercises the real main() dispatch for both True/False. 0 blocking.

@exiao
exiao merged commit 54c24cb into live-config Jul 2, 2026
37 checks passed
@exiao
exiao deleted the fix/kanban-cli-exit-codes branch July 2, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant