How Worktrunk Ensures Consistent Subprocess Logging and Explicit Untracing
Worktrunk enforces consistent subprocess logging by wrapping every external command in a Cmd wrapper that instantiates a CommandTrace object, which panics in its Drop implementation unless explicitly resolved via complete() or fail(), ensuring every foreground subprocess is recorded in trace.jsonl while background processes are deliberately excluded.
The max-sixty/worktrunk repository implements a rigorous tracing system that eliminates silent subprocess execution. By leveraging Rust's ownership model and the must-resolve pattern, Worktrunk guarantees that every foreground command appears in the unified trace log, while making untraced background jobs an explicit, documented design choice.
The CommandTrace Must-Resolve Pattern
At the core of Worktrunk's consistent subprocess logging is the CommandTrace type defined in src/trace/emit.rs. This structure enforces a strict contract where every trace instance must be explicitly resolved before the object is dropped, preventing any command from executing without generating a trace record.
Automatic Instantiation in the Cmd Wrapper
Every external command execution begins with the Cmd wrapper in src/shell_exec.rs. At line 1570, the code instantiates CommandTrace::new(context, cmd_str), capturing the command string and operational context immediately upon construction.
// Simplified representation of the pattern in src/shell_exec.rs
let mut trace = CommandTrace::new(Some("worktree"), "git status");
let output = Cmd::new("git")
.args(["status", "--porcelain"])
.run()?;
Explicit Resolution Paths
The CommandTrace type requires developers to choose between two terminal states. After a successful execution, the code invokes CommandTrace::complete(dur_us, true) at line 1711 of src/shell_exec.rs, writing a completed record with microsecond duration to the trace log. When execution fails, CommandTrace::fail(dur_us, err) at line 1727 records the error state and failure details.
// Resolution after successful execution
trace.complete(1200, true)?; // duration: 1200µs, success: true
// Resolution after error
trace.complete(500, false)?; // Or using fail() for explicit error handling
Panic-on-Drop Enforcement
The enforcement mechanism resides in src/trace/emit.rs at lines 352-360. The Drop implementation asserts that the trace has been resolved; if neither complete() nor fail() was called before the object goes out of scope, the program panics with a clear error message. This compile-time and runtime guarantee ensures no foreground subprocess can silently bypass logging.
Distinguishing Traced Foreground from Untraced Background Processes
Worktrunk differentiates between foreground commands that must be logged and background processes that should remain untraced. This distinction is implemented through deliberate structural choices in the codebase.
Foreground Subprocess Coverage
Every higher-level component that spawns external tools creates a CommandTrace before invoking Cmd. Examples include:
- Git plumbing commands at
src/git/repository/mod.rsline 1795, where repository operations are fully traced. - Concurrent pipelines at
src/output/concurrent.rsline 283, ensuring parallel child processes each generate distinct trace entries. - Custom user commands at
src/commands/custom.rsline 146, applying the same logging discipline to arbitrary external tools.
Deliberate Exclusion of Background Jobs
Detached child processes, such as background jobs launched via src/commands/process.rs, are intentionally not wrapped in Cmd. These processes bypass CommandTrace creation entirely, excluding them from trace.jsonl as documented in the surrounding code comments. This design accommodates long-running daemons that outlive the parent Worktrunk process or interactive helper applications where tracing provides no value.
use std::process::Command;
// Example: a background job that is intentionally untraced
// This spawns a detached child without CommandTrace
Command::new("some-long-running-daemon")
.arg("--quiet")
.spawn()?; // Intentionally omitted from trace.jsonl
Practical Implementation Examples
The following examples illustrate how Worktrunk implements the tracing contract internally and how background processes avoid it:
use worktrunk::shell_exec::Cmd;
use worktrunk::trace::CommandTrace;
// Foreground command with mandatory trace resolution
let mut trace = CommandTrace::new(Some("worktree"), "git status");
let output = Cmd::new("git")
.args(["status", "--porcelain"])
.current_dir("/path/to/worktree")
.run()?;
// Explicit success logging is required to avoid panic on drop
trace.complete(1200, true)?;
use std::process::Command;
// Background daemon that is explicitly untraced
// No CommandTrace is created, so this process is omitted from trace.jsonl
Command::new("persistent-worker")
.arg("--detach")
.spawn()?;
Summary
CommandTraceis a must-resolve type that panics on drop if not completed, ensuring no subprocess executes without a trace entry.- Foreground subprocesses are uniformly logged through the
Cmdwrapper, with coverage extending across Git operations, concurrent pipelines, and custom commands. - Background processes are explicitly excluded by bypassing the
Cmdwrapper insrc/commands/process.rs, making untraced execution a deliberate architectural choice. - Trace data aggregates to
trace.jsonl, providing a unified log for debugging, profiling, and reproducibility across all traced operations.
Frequently Asked Questions
What happens if a developer forgets to resolve a CommandTrace?
The application will panic when the CommandTrace object is dropped. According to the implementation in src/trace/emit.rs at lines 352-360, the Drop implementation asserts that either complete() or fail() was called, preventing any silent omissions from the trace log.
Can background processes be traced if needed?
Technically yes, but it requires wrapping them in the Cmd structure instead of using raw std::process::Command. However, the codebase intentionally avoids this for detached children in src/commands/process.rs because background jobs often outlive the parent process, making trace correlation difficult and potentially misleading.
Where does Worktrunk store subprocess trace data?
All resolved CommandTrace records are written to a trace.jsonl file in JSON Lines format. This centralized log aggregates timing, exit status, and command metadata from every traced foreground subprocess across the entire Worktrunk session.
How does CommandTrace handle concurrent subprocess execution?
Each concurrent pipeline child receives its own CommandTrace instance. As shown in src/output/concurrent.rs at line 283, parallel subprocesses are traced individually, with each maintaining its own resolution contract to ensure complete logging even during concurrent execution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →