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.
This commit is contained in:
l0ng-ai
2026-08-16 12:16:18 +08:00
parent 2b4067f925
commit cb07fd45e7
2 changed files with 40 additions and 5 deletions
+33
View File
@@ -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<i32> {
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();
+7 -5
View File
@@ -18,7 +18,7 @@ Accepted anywhere on the line, before or after the subcommand.
| Flag | Effect |
|---|---|
| `-m, --machine <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]`