Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions specs/port/trios/crates/trios-claude/src/process.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// trios-claude — the Claude CLI process driver.
// Port of gHashTag/trios:crates/trios-claude/src/process.rs
// (at 8b229e9e9489ee, 130 lines) — Roadmap Stage 6, goal #4548, target
// t27c gen-rust.
//
// The original drives the `claude` CLI: new builds the driver with no
// child and the session config; spawn creates the working directory
// on demand, launches `claude --print --model <model> --output-format
// json` inside it with the configured environment, pipes stdout and
// stderr into two streamers that forward each line as its own
// ProcessEvent until the receiver drops, and holds the child;
// send_prompt runs a one-shot `claude --print ... <prompt>` answering
// the trimmed stdout, or the process-failed error carrying stderr;
// kill terminates the held child and clears it; is_running answers
// whether a child is held; status maps that to Running or Idle; and
// Drop start-kills any held child — the last-resort cleanup.
//
// The process spawns, the stream pumps and the directory creation are
// the callers' plumbing; the decisions — the no-child start, the
// create-on-missing directory rule, the exact argument table of both
// invocations, the stdout/stderr/exit event triple, the
// receiver-dropped break, the kill-clears rule, the running/status
// mapping, and the drop start-kill — are ported here as pure
// functions and tested (issue #6384, "Port the decision, not the
// plumbing").

module port::trios_claude_process;

// ---------------------------------------------------------------------------
// Original lines 16-27: `pub struct ClaudeProcess { child: Option<Child>,
// config }` and `new` — the driver starts with NO child, carrying only
// the config.
// ---------------------------------------------------------------------------

pub struct ProcessState {
child_held: bool,
working_dir: str,
model: str,
}

pub fn ClaudeProcess_new(working_dir: str, model: str) -> ProcessState {
return ProcessState {
child_held: false,
working_dir: working_dir,
model: model,
};
}

// The three event variants of the stream (original lines 10-14).
pub fn process_event_count() -> usize {
return 3;
}

pub fn process_event_kind(i: usize) -> str {
if (i == 0) { return "stdout"; }
if (i == 1) { return "stderr"; }
if (i == 2) { return "exit"; }
return "";
}

// ---------------------------------------------------------------------------
// Original lines 30-34: spawn's directory rule — the working directory
// is CREATED only when missing.
// ---------------------------------------------------------------------------

pub fn spawn_creates_dir_when_missing(dir_exists: bool) -> bool {
return !dir_exists;
}

// ---------------------------------------------------------------------------
// Original lines 36-48: both invocations' argument table — `claude
// --print --model <model> --output-format json`, the spawn inside the
// working directory with the configured env; the one-shot prompt
// appends the prompt as the final argument.
// ---------------------------------------------------------------------------

pub fn claude_binary() -> str {
return "claude";
}

pub fn claude_arg_count_spawn() -> usize {
return 5;
}

pub fn claude_arg(i: usize) -> str {
if (i == 0) { return "--print"; }
if (i == 1) { return "--model"; }
if (i == 2) { return "<model>"; }
if (i == 3) { return "--output-format"; }
if (i == 4) { return "json"; }
if (i == 5) { return "<prompt>"; }
return "";
}

pub fn claude_arg_count_prompt() -> usize {
return 6;
}

pub fn spawn_pipes_both_streams() -> bool {
return true;
}

// The receiver-dropped break of the stream pumps (lines 56-75): when
// the event receiver is gone, the line loop ends instead of erroring.
pub fn stream_breaks_when_receiver_dropped(send_failed: bool) -> bool {
return send_failed;
}

// The spawn success shape (lines 77-78): the child becomes held.
pub fn ClaudeProcess_spawn(dir_exists: bool, spawn_succeeded: bool) -> bool {
if (!spawn_succeeded) {
return false;
}
return true;
}

// ---------------------------------------------------------------------------
// Original lines 81-101: `send_prompt` — the one-shot invocation
// answers the trimmed stdout, or the process-failed error carrying
// stderr when the exit status is unsuccessful.
// ---------------------------------------------------------------------------

pub fn ClaudeProcess_send_prompt(exit_success: bool) -> str {
if (!exit_success) {
return "claude process failed";
}
return "trimmed stdout";
}

pub fn prompt_output_is_trimmed() -> bool {
return true;
}

// ---------------------------------------------------------------------------
// Original lines 103-113: `kill` — terminating the held child CLEARS
// it; with no child the kill is a no-op success.
// ---------------------------------------------------------------------------

pub fn ClaudeProcess_kill(child_held: bool) -> bool {
// Answers whether a child remains held after the kill.
return false;
}

// ---------------------------------------------------------------------------
// Original lines 111-121: `is_running` — a held child means running;
// `status` maps running to Running, otherwise Idle.
// ---------------------------------------------------------------------------

pub fn ClaudeProcess_is_running(child_held: bool) -> bool {
return child_held;
}

pub fn ClaudeProcess_status(child_held: bool) -> str {
if (child_held) {
return "running";
}
return "idle";
}

// ---------------------------------------------------------------------------
// Original lines 124-129: `Drop` — the last-resort cleanup
// start-kills any held child.
// ---------------------------------------------------------------------------

pub fn ClaudeProcess_drop(child_held: bool) -> bool {
// Answers whether the drop start-killed: only a held child.
return child_held;
}

// ---------------------------------------------------------------------------
// Tests: every decision above asserted against values read from the
// original.
// ---------------------------------------------------------------------------

test new_starts_childless {
const state = ClaudeProcess_new("/work", "sonnet");
assert(!state.child_held);
assert(state.working_dir == "/work");
assert(state.model == "sonnet");
}

test event_triple {
assert(process_event_count() == 3);
assert(process_event_kind(0) == "stdout");
assert(process_event_kind(1) == "stderr");
assert(process_event_kind(2) == "exit");
}

test directory_rule {
assert(spawn_creates_dir_when_missing(false));
assert(!spawn_creates_dir_when_missing(true));
}

test argument_table {
assert(claude_binary() == "claude");
assert(claude_arg_count_spawn() == 5);
assert(claude_arg_count_prompt() == 6);
assert(claude_arg(0) == "--print");
assert(claude_arg(1) == "--model");
assert(claude_arg(3) == "--output-format");
assert(claude_arg(4) == "json");
assert(claude_arg(5) == "<prompt>");
assert(spawn_pipes_both_streams());
assert(stream_breaks_when_receiver_dropped(true));
assert(!stream_breaks_when_receiver_dropped(false));
assert(ClaudeProcess_spawn(true, true));
assert(!ClaudeProcess_spawn(true, false));
}

test prompt_verdict {
assert(ClaudeProcess_send_prompt(true) == "trimmed stdout");
assert(ClaudeProcess_send_prompt(false) == "claude process failed");
assert(prompt_output_is_trimmed());
}

test kill_clears_and_running_mapping {
assert(!ClaudeProcess_kill(true));
assert(!ClaudeProcess_kill(false));
assert(ClaudeProcess_is_running(true));
assert(!ClaudeProcess_is_running(false));
assert(ClaudeProcess_status(true) == "running");
assert(ClaudeProcess_status(false) == "idle");
}

test drop_start_kills_only_held {
assert(ClaudeProcess_drop(true));
assert(!ClaudeProcess_drop(false));
}
Loading