rtb-error process exit-code attachment¶
Status: APPROVED — GTB-parity gap from the v0.22.0 audit (Phase 2,
§A; shared-subsystem drift). RTB exits 1 on every error; GTB threads
custom process exit codes (pkg/errorhandling/exitcode.go:23,
WithExitCode/ExitCode, e.g. 128+signum). This adds the capability the
RTB way — a value attached to an error and read once at the edge,
not a re-introduction of the rejected ErrorHandler.check() funnel
(CLAUDE.md anti-patterns; rtb-error/src/lib.rs:6).
1. Surface¶
/// Attach a process exit code to any error/report.
pub trait WithExitCode {
fn with_exit_code(self, code: u8) -> ExitCoded<Self> where Self: Sized;
}
/// Carries an exit code alongside a diagnostic; renders transparently.
pub struct ExitCoded<E> { pub code: u8, pub source: E }
/// Read the attached code from a Report at the process boundary.
pub fn exit_code_of(report: &miette::Report) -> Option<u8>;
ExitCoded<E>implementsstd::error::Error+miette::Diagnostictransparently (delegatescode/help/severity/source to the inner diagnostic) so attaching a code never hides the underlying diagnostic output.WithExitCodeis blanket-impl'd forE: Into<Report>/Error.- Convenience:
miette-style —err.with_exit_code(2)?.
2. Mechanism (read once at the edge)¶
rtb-cli's Application::run already renders the final Result through
the miette hook. Extend that single boundary: when the run returns
Err(report), call exit_code_of(&report); if Some(code), after
rendering, std::process::exit(code) (or return ExitCode::from(code));
otherwise the existing default (1). No code path other than the boundary
inspects the attachment — errors stay values propagated with ?.
exit_code_of recovers the code by downcasting the report's inner error to
ExitCoded<_> (miette Report::downcast_ref against the carried type, or
a small object-safe ExitCodeCarrier marker trait the struct implements so
the boundary can read code without knowing E).
3. Non-goals / boundaries¶
- No
ErrorHandler/.check()funnel —with_exit_codedecorates a value; it does not centralise error handling. - No signal-to-code policy baked in (callers choose codes; a
128+signumhelper may be added by a future shutdown spec, not here). 0is not attachable as an "error" code (success isOk).
4. Testing (TDD, ≥90%)¶
err.with_exit_code(2)→exit_code_ofreturnsSome(2); the inner diagnostic'scode()/help()/Displayare unchanged (transparency).- A plain error →
exit_code_ofreturnsNone(boundary defaults to1). - Nested:
with_exit_codethrough a?-propagated chain still surfaces at the boundary. - An
Application::runintegration test (or a thin harness) asserts the process exits with the attached code for anExitCodederror and1otherwise.
5. Out of scope¶
- Per-command default exit-code maps; signal-derived codes (future).