Skip to main content

locode_exec/
output.rs

1//! The crate's ONLY stdout/stderr writers (ADR-0009 stdout discipline).
2//!
3//! Everything else in the crate is denied `print_stdout`/`print_stderr` by the
4//! workspace lints; the narrow `#[allow]`s below are the audited exceptions —
5//! exactly Codex-exec's pattern (named, narrow emitters; the crate-wide deny
6//! stays intact).
7
8use std::process::ExitCode;
9
10use locode_core::Status;
11
12/// Write one JSON value as one stdout line (the report, or one stream event).
13///
14/// A serialize failure (realistically unreachable — every field is plain data)
15/// emits a `{"type":"error"}` object instead, so stdout still carries exactly
16/// one machine-readable line (Codex's jsonl fallback). Write errors (EPIPE
17/// from `| head`, a closed pipe) are deliberately ignored — panicking on a
18/// consumer closing the pipe would be wrong for a CLI.
19pub fn write_json_line(value: &impl serde::Serialize) {
20    use std::io::Write;
21    let line = serde_json::to_string(value).unwrap_or_else(|e| {
22        format!(r#"{{"type":"error","message":"failed to serialize output: {e}"}}"#)
23    });
24    let mut stdout = std::io::stdout().lock();
25    let _ = writeln!(stdout, "{line}");
26    let _ = stdout.flush();
27}
28
29/// Write the `text`-mode artifact (the final assistant message).
30pub fn write_text(text: &str) {
31    use std::io::Write;
32    let mut stdout = std::io::stdout().lock();
33    let _ = writeln!(stdout, "{text}");
34    let _ = stdout.flush();
35}
36
37/// Write a pre-run failure to stderr (`error: …`), Codex's pre-run pattern.
38pub fn error_line(message: &str) {
39    use std::io::Write;
40    let _ = writeln!(std::io::stderr().lock(), "error: {message}");
41}
42
43/// Write a non-fatal diagnostic to stderr (`warning: …`) — settings-layer
44/// degradations (ADR-0024: skipped layers, stripped keys, invalid entries).
45pub fn warning_line(message: &str) {
46    use std::io::Write;
47    let _ = writeln!(std::io::stderr().lock(), "warning: {message}");
48}
49
50/// ADR-0009 exit-code mapping: 0 for any **structured** terminal state
51/// (`completed`/`max_turns`/`cancelled` — the run produced a valid report,
52/// ADR-0018), 1 for fatal (`model_error`/`error`); clap owns exit 2 for usage
53/// errors. `Status` is `#[non_exhaustive]` under the additive envelope policy
54/// (`schema_version: 1`): unknown future statuses map conservatively to 1.
55pub fn exit_code(status: Status) -> ExitCode {
56    match status {
57        Status::Completed | Status::MaxTurns | Status::Cancelled => ExitCode::SUCCESS,
58        // `model_error`/`error` — and, per the additive policy, any unknown
59        // future status — map conservatively to failure.
60        _ => ExitCode::from(1),
61    }
62}
63
64/// The notice shown when `--restricted` is used.
65///
66/// Restricted mode predates the permission rules that would make it usable: the
67/// approval seam exists (ADR-0017) but there is no allow/deny store behind it, so
68/// an answer cannot be remembered and every call asks again. Saying so is the
69/// truthful-user-facing-text rule (AGENTS.md) — a half-built mode that looks
70/// finished is worse than one that announces itself.
71pub const RESTRICTED_MODE_NOTICE: &str = "--restricted is incomplete: file access is limited to the working directory \
72     and every tool call asks for approval, but answers cannot be saved yet, so \
73     the same call asks again each time. Omit the flag to run without either.";
74
75/// The notice shown on a normal (unrestricted) run.
76///
77/// Kept to one line: the default changed to "no jail, no prompts", and a user who
78/// does not know that cannot make an informed choice about where they run this.
79pub const UNRESTRICTED_MODE_NOTICE: &str = "running without approval prompts, and with file access outside the working \
80     directory; pass --restricted to limit both.";
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn exit_codes_map_all_statuses() {
88        assert_eq!(exit_code(Status::Completed), ExitCode::SUCCESS);
89        assert_eq!(exit_code(Status::MaxTurns), ExitCode::SUCCESS);
90        assert_eq!(exit_code(Status::Cancelled), ExitCode::SUCCESS);
91        assert_eq!(exit_code(Status::ModelError), ExitCode::from(1));
92        assert_eq!(exit_code(Status::Error), ExitCode::from(1));
93    }
94}