From cb07fd45e7c5457401cf40d2d3f0aa6118338457 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:16:18 +0800 Subject: [PATCH] docs(cli): correct two claims about --json that scripts branch on Both found by running the verbs rather than reading them, and one of them is mine from four commits ago. `--json` said two verbs still print JSON when they fail. There are three: `doctor` prints its whole report on the exit-1 path, deliberately, because an unreachable server is the finding and `tty7 doctor || alert` needs the rows as well as the code. My earlier batch simply had not included it -- this time every verb that can fail was run with --json, and the other seventeen do print nothing. The `run` section was worse. It said a signal death reports `exit_code_known: true` "because a status was read", and concluded that `run` therefore cannot tell a command the OOM killer took from one that exited 1 on its own. The flag is false, and it is exactly that distinction: $ tty7 run --json -- sh -c 'kill -9 $$' {"pane":14,"exit":1,"exit_code_known":false,"kept":false} `reported_exit_code` returns `None` for a status that spells itself "Terminated by", `run` turns that into an exit 1 plus a note on stderr, and an orchestrator can branch on the flag without reading the pane. The page had been telling it to do the opposite. So the daemon side now carries a comment saying why a signal is not dressed up as `128 + signal` -- that convention belongs to the shell, and inventing it here hands back a number the command never returned -- and a test pins `None` for a signal against `Some(code)` for an exit, including the "Terminated by" spelling the whole thing turns on. --- crates/tty7-core/src/daemon/pane.rs | 33 +++++++++++++++++++++++++++++ docs/cli/reference.mdx | 12 ++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/crates/tty7-core/src/daemon/pane.rs b/crates/tty7-core/src/daemon/pane.rs index 9454e2bc..be8c8369 100644 --- a/crates/tty7-core/src/daemon/pane.rs +++ b/crates/tty7-core/src/daemon/pane.rs @@ -756,6 +756,13 @@ impl Drop for SigtermUnblocked { } } +/// The exit code to report, or `None` when the pane did not leave one. +/// +/// A signal death is `None` rather than `128 + signal`: the shell convention +/// is the *shell's*, and inventing it here would hand a caller a number the +/// command never returned. `tty7 run` turns `None` into an exit 1 that says +/// so — on stderr, and as `exit_code_known: false` — which is what lets an +/// orchestrator tell a command the OOM killer took from one that exited 1. fn reported_exit_code(status: &portable_pty::ExitStatus) -> Option { match status.to_string().starts_with("Terminated by") { true => None, @@ -4328,6 +4335,32 @@ mod tests { assert!(hex_val(b'/').is_none()); } + /// A signal is not an exit code, and must not be dressed up as one. + /// + /// `run` reports `exit_code_known: false` off the back of this `None`, and + /// the reference tells orchestrators to branch on that flag — it said the + /// opposite for a while, so this is here to keep the two agreeing. + #[test] + fn a_pane_killed_by_a_signal_reports_no_exit_code() { + assert_eq!( + reported_exit_code(&portable_pty::ExitStatus::with_exit_code(0)), + Some(0) + ); + assert_eq!( + reported_exit_code(&portable_pty::ExitStatus::with_exit_code(42)), + Some(42) + ); + + // How portable_pty spells a signal death, and the only thing that + // separates one from an ordinary code here. + let signalled = portable_pty::ExitStatus::with_signal("SIGKILL"); + assert!( + signalled.to_string().starts_with("Terminated by"), + "the spelling this turns on changed: {signalled}" + ); + assert_eq!(reported_exit_code(&signalled), None); + } + #[test] fn osc133_exit_code_parsing() { let mut s = OscSniffer::new(); diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index 2089b69e..06a9837e 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -18,7 +18,7 @@ Accepted anywhere on the line, before or after the subcommand. | Flag | Effect | |---|---| | `-m, --machine ` | Route to a linked machine over the local server's existing link. Matches the full link key (`me@devbox:22`) or the bare host (`devbox`). SSH links only; a down link, or a jump/proxy chain, is refused with a reason rather than dialled fresh. | -| `--json` | One JSON object on stdout instead of the human table. A verb that *fails* prints its message on stderr and no JSON at all, so a reader parsing stdout must check the exit code first. Two verbs are exceptions, because there the bad news is the answer: `pane close` still prints `{"closed":[…],"failed":[…]}`, and `wait` still prints the state it gave up in. (`run` is not an exception — a child exiting nonzero is the verb succeeding, and its JSON is printed as usual.) | +| `--json` | One JSON object on stdout instead of the human table. A verb that *fails* prints its message on stderr and no JSON at all, so a reader parsing stdout must check the exit code first. Three verbs are exceptions, because there the bad news is the answer: `pane close` still prints `{"closed":[…],"failed":[…]}`, `wait` still prints the state it gave up in, and `doctor` still prints its whole report — an unreachable server is the finding, and `tty7 doctor || alert` needs both the exit code and the rows. (`run` is not an exception — a child exiting nonzero is the verb succeeding, and its JSON is printed as usual.) | | `-q, --quiet` | No output on success. Errors still go to stderr. | ## Environment @@ -94,10 +94,12 @@ for the server to read in the short window it waits. `run` then exits 1 as a stand-in and says so on stderr, and this flag is how a caller tells that 1 from a command that really did exit 1. -A command **killed by a signal** also reports `exit: 1` — not the `128+N` a -shell would give — but with `exit_code_known: true`, because a status was read. -So `run` cannot distinguish a command the OOM killer took from one that exited -1 on its own; read the pane's output if that difference matters. +A command **killed by a signal** reports `exit: 1` — not the `128+N` a shell +would give — with `exit_code_known: false` and the same stderr note as any +other unknown code, because a status that says "terminated by a signal" is not +an exit code the daemon will invent one from. That flag is exactly how a +caller tells a command the OOM killer took from one that exited 1 on its own, +so an orchestrator does not have to read the pane to find out. ### `tty7 new [PATH] [--open]`