Skip to content

Commit 09a5d20

Browse files
committed
fix!: provider wrappers — read prompt from RALPHTERM_PROMPT_FILE, write to RALPHTERM_OUTPUT_FILE (real reviewer-hang fix) (v0.4.11)
ACTUAL root cause of the multi-hour review-stage hang the user reported (v0.4.10's paste-size fix was orthogonal): The default reviewer is scripts/wrappers/codex.sh. The wrapper did: prompt=\$(cat) ... "\$PROVIDER_CMD" exec ... "\$prompt" ... \`cat\` reads stdin until EOF. drive_agent's PTY writer never closes (it stays alive so we can send /exit at teardown). Result: cat deadlocks on the very first line of the wrapper. codex never spawns. Zero PTY bytes. Spinner climbs idle indefinitely. This is a regression vector that was created when v0.3 introduced drive_agent — drive_agent's keep-writer-open contract is incompatible with stdin-reading wrappers. Implementer fixtures (fake-agent.sh etc.) were updated to dual-mode (RALPHTERM_PROMPT_FILE env var) but the production wrappers were missed. Rewrote all four wrappers (codex.sh, copilot.sh, gemini.sh, opencode.sh) to be drive_agent-aware: 1. Prompt: read from \$RALPHTERM_PROMPT_FILE if set (no stdin touch → no deadlock). Fall back to stdin for the legacy non-drive_agent call path. 2. Output: capture the provider's stdout/stderr to a tempfile. Mirror it to stdout (live transcript stays useful). If \$RALPHTERM_OUTPUT_FILE is set, write the captured output between <<<BEGIN>>>/<<<END>>> markers with a trailing REVIEW_PASS or REVIEW_FAIL rc=N — exactly what review_phases.rs::external_review_decision looks for. Implementer path (claude direct) unaffected — those go through drive_agent's bracketed-paste, not a wrapper script. Tests stay green (16/17 fake-agent, all wrappers_compat, all mode_compat, etc.).
1 parent 9144be8 commit 09a5d20

6 files changed

Lines changed: 133 additions & 31 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ralphterm"
3-
version = "0.4.10"
3+
version = "0.4.11"
44
edition = "2021"
55
rust-version = "1.85"
66
description = "Programmable PTY for the official Claude Code and Codex CLIs. Runs long multi-task plans unattended — iterates, validates, commits per task, and gates merge behind a parallel review pipeline."

scripts/wrappers/codex.sh

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,31 @@
55
# Auth env:
66
# OPENAI_API_KEY - required for codex to authenticate against OpenAI APIs
77
# Optional env:
8-
# CLAUDE_MODEL - forwarded as `--model <value>` so plans authored for
9-
# ralphex's model selection knob keep working.
8+
# CLAUDE_MODEL - forwarded as `--model <value>`.
109
# PROVIDER_OVERRIDE - override the binary name (mostly for tests).
1110
#
12-
# Behaviour: reads the prompt from stdin and invokes `codex exec
13-
# "<prompt>"` (codex's non-interactive subcommand). Bare `codex` requires
14-
# a TTY and refuses to read piped stdin, so the wrapper must use `exec`.
15-
# Emits COMPLETED on success or FAILED on a non-zero exit.
11+
# IO contract: this wrapper supports both ralphterm's v0.3+ file-handoff
12+
# (env vars RALPHTERM_PROMPT_FILE + RALPHTERM_OUTPUT_FILE) AND the
13+
# legacy stdin/stdout streaming path. Drive_agent (v0.4+) sets the env
14+
# vars; older callers pipe the prompt on stdin and watch for COMPLETED
15+
# on stdout.
16+
#
17+
# The legacy `prompt=$(cat)` path used to deadlock against drive_agent
18+
# because drive_agent's PTY writer never closes (it keeps the writer
19+
# alive to send /exit on teardown). With RALPHTERM_PROMPT_FILE set we
20+
# read the prompt from disk and never touch the PTY stdin.
1621
set -eu
1722

1823
PROVIDER_CMD="${PROVIDER_OVERRIDE:-codex}"
1924

20-
prompt=$(cat)
25+
if [ -n "${RALPHTERM_PROMPT_FILE:-}" ]; then
26+
prompt=$(cat "$RALPHTERM_PROMPT_FILE")
27+
else
28+
prompt=$(cat)
29+
fi
30+
2131
if [ -z "${prompt}" ]; then
22-
printf 'FAILED: no prompt on stdin\n' >&2
32+
printf 'FAILED: no prompt provided\n' >&2
2333
exit 1
2434
fi
2535

@@ -30,24 +40,47 @@ if [ -n "${CLAUDE_MODEL:-}" ]; then
3040
model_arg="--model ${CLAUDE_MODEL}"
3141
fi
3242

33-
# If PROVIDER_OVERRIDE is set, the test shim expects the prompt on stdin —
34-
# stream it through instead of passing as argv so the shim's
35-
# `prompt=$(cat)` pattern keeps working. Otherwise drive the real codex
36-
# via its non-interactive `exec` subcommand with the prompt as the final
37-
# argv, and close stdin so codex doesn't block waiting for input that
38-
# never comes (the PTY stdin stays open otherwise).
43+
# Capture codex's output to a tempfile so we can both stream it to the
44+
# PTY (for live transcripts) and write it into the file-handoff target.
45+
codex_out=$(mktemp)
46+
cleanup() { rm -f "$codex_out"; }
47+
trap 'kill "${child:-0}" 2>/dev/null || true; cleanup; exit 130' INT TERM
48+
3949
if [ -n "${PROVIDER_OVERRIDE:-}" ]; then
4050
# shellcheck disable=SC2086
41-
printf '%s\n' "$prompt" | "$PROVIDER_CMD" $model_arg &
51+
printf '%s\n' "$prompt" | "$PROVIDER_CMD" $model_arg > "$codex_out" 2>&1 &
4252
else
4353
# shellcheck disable=SC2086
44-
"$PROVIDER_CMD" exec $model_arg "$prompt" </dev/null &
54+
"$PROVIDER_CMD" exec $model_arg "$prompt" </dev/null > "$codex_out" 2>&1 &
4555
fi
4656
child=$!
4757
set +e
4858
wait "$child"
4959
rc=$?
5060
set -e
61+
62+
# Mirror codex's output to the PTY so the live transcript file (and
63+
# any --serve dashboard watching the PTY) sees what codex said.
64+
cat "$codex_out"
65+
66+
if [ -n "${RALPHTERM_OUTPUT_FILE:-}" ]; then
67+
# Write the file-handoff response. drive_agent's review decision
68+
# logic recognises REVIEW_PASS / NO ISSUES FOUND on success and
69+
# REVIEW_FAIL on failure (src/review_phases.rs::external_review_decision).
70+
{
71+
printf '<<<BEGIN>>>\n'
72+
cat "$codex_out"
73+
printf '\n'
74+
if [ "$rc" -eq 0 ]; then
75+
printf 'REVIEW_PASS\n'
76+
else
77+
printf 'REVIEW_FAIL rc=%s\n' "$rc"
78+
fi
79+
printf '<<<END>>>\n'
80+
} > "$RALPHTERM_OUTPUT_FILE"
81+
fi
82+
83+
cleanup
5184
if [ "$rc" -eq 0 ]; then
5285
printf '\nCOMPLETED\n'
5386
else

scripts/wrappers/copilot.sh

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ set -eu
1717

1818
PROVIDER_CMD="${PROVIDER_OVERRIDE:-gh}"
1919

20-
prompt=$(cat)
20+
if [ -n "${RALPHTERM_PROMPT_FILE:-}" ]; then
21+
prompt=$(cat "$RALPHTERM_PROMPT_FILE")
22+
else
23+
prompt=$(cat)
24+
fi
25+
2126
if [ -z "${prompt}" ]; then
22-
printf 'FAILED: no prompt on stdin\n' >&2
27+
printf 'FAILED: no prompt provided\n' >&2
2328
exit 1
2429
fi
2530

2631
tmpfile=$(mktemp)
27-
trap 'rm -f "$tmpfile"' EXIT
32+
provider_out=$(mktemp)
33+
trap 'rm -f "$tmpfile" "$provider_out"' EXIT
2834
trap 'kill "${child:-0}" 2>/dev/null || true; exit 130' INT TERM
2935

3036
printf '%s\n' "$prompt" > "$tmpfile"
@@ -36,16 +42,33 @@ fi
3642

3743
if [ "$PROVIDER_CMD" = "gh" ]; then
3844
# shellcheck disable=SC2086
39-
"$PROVIDER_CMD" copilot suggest $model_arg < "$tmpfile" &
45+
"$PROVIDER_CMD" copilot suggest $model_arg < "$tmpfile" > "$provider_out" 2>&1 &
4046
else
4147
# shellcheck disable=SC2086
42-
"$PROVIDER_CMD" $model_arg < "$tmpfile" &
48+
"$PROVIDER_CMD" $model_arg < "$tmpfile" > "$provider_out" 2>&1 &
4349
fi
4450
child=$!
4551
set +e
4652
wait "$child"
4753
rc=$?
4854
set -e
55+
56+
cat "$provider_out"
57+
58+
if [ -n "${RALPHTERM_OUTPUT_FILE:-}" ]; then
59+
{
60+
printf '<<<BEGIN>>>\n'
61+
cat "$provider_out"
62+
printf '\n'
63+
if [ "$rc" -eq 0 ]; then
64+
printf 'REVIEW_PASS\n'
65+
else
66+
printf 'REVIEW_FAIL rc=%s\n' "$rc"
67+
fi
68+
printf '<<<END>>>\n'
69+
} > "$RALPHTERM_OUTPUT_FILE"
70+
fi
71+
4972
if [ "$rc" -eq 0 ]; then
5073
printf '\nCOMPLETED\n'
5174
else

scripts/wrappers/gemini.sh

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ set -eu
1717

1818
PROVIDER_CMD="${PROVIDER_OVERRIDE:-gemini}"
1919

20-
prompt=$(cat)
20+
if [ -n "${RALPHTERM_PROMPT_FILE:-}" ]; then
21+
prompt=$(cat "$RALPHTERM_PROMPT_FILE")
22+
else
23+
prompt=$(cat)
24+
fi
25+
2126
if [ -z "${prompt}" ]; then
22-
printf 'FAILED: no prompt on stdin\n' >&2
27+
printf 'FAILED: no prompt provided\n' >&2
2328
exit 1
2429
fi
2530

2631
tmpfile=$(mktemp)
27-
trap 'rm -f "$tmpfile"' EXIT
32+
provider_out=$(mktemp)
33+
trap 'rm -f "$tmpfile" "$provider_out"' EXIT
2834
trap 'kill "${child:-0}" 2>/dev/null || true; exit 130' INT TERM
2935

3036
printf '%s\n' "$prompt" > "$tmpfile"
@@ -35,12 +41,29 @@ if [ -n "${CLAUDE_MODEL:-}" ]; then
3541
fi
3642

3743
# shellcheck disable=SC2086
38-
"$PROVIDER_CMD" $model_arg < "$tmpfile" &
44+
"$PROVIDER_CMD" $model_arg < "$tmpfile" > "$provider_out" 2>&1 &
3945
child=$!
4046
set +e
4147
wait "$child"
4248
rc=$?
4349
set -e
50+
51+
cat "$provider_out"
52+
53+
if [ -n "${RALPHTERM_OUTPUT_FILE:-}" ]; then
54+
{
55+
printf '<<<BEGIN>>>\n'
56+
cat "$provider_out"
57+
printf '\n'
58+
if [ "$rc" -eq 0 ]; then
59+
printf 'REVIEW_PASS\n'
60+
else
61+
printf 'REVIEW_FAIL rc=%s\n' "$rc"
62+
fi
63+
printf '<<<END>>>\n'
64+
} > "$RALPHTERM_OUTPUT_FILE"
65+
fi
66+
4467
if [ "$rc" -eq 0 ]; then
4568
printf '\nCOMPLETED\n'
4669
else

scripts/wrappers/opencode.sh

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,20 @@ set -eu
1818

1919
PROVIDER_CMD="${PROVIDER_OVERRIDE:-opencode}"
2020

21-
prompt=$(cat)
21+
if [ -n "${RALPHTERM_PROMPT_FILE:-}" ]; then
22+
prompt=$(cat "$RALPHTERM_PROMPT_FILE")
23+
else
24+
prompt=$(cat)
25+
fi
26+
2227
if [ -z "${prompt}" ]; then
23-
printf 'FAILED: no prompt on stdin\n' >&2
28+
printf 'FAILED: no prompt provided\n' >&2
2429
exit 1
2530
fi
2631

2732
tmpfile=$(mktemp)
28-
trap 'rm -f "$tmpfile"' EXIT
33+
provider_out=$(mktemp)
34+
trap 'rm -f "$tmpfile" "$provider_out"' EXIT
2935
trap 'kill "${child:-0}" 2>/dev/null || true; exit 130' INT TERM
3036

3137
printf '%s\n' "$prompt" > "$tmpfile"
@@ -36,12 +42,29 @@ if [ -n "${CLAUDE_MODEL:-}" ]; then
3642
fi
3743

3844
# shellcheck disable=SC2086
39-
"$PROVIDER_CMD" $model_arg < "$tmpfile" &
45+
"$PROVIDER_CMD" $model_arg < "$tmpfile" > "$provider_out" 2>&1 &
4046
child=$!
4147
set +e
4248
wait "$child"
4349
rc=$?
4450
set -e
51+
52+
cat "$provider_out"
53+
54+
if [ -n "${RALPHTERM_OUTPUT_FILE:-}" ]; then
55+
{
56+
printf '<<<BEGIN>>>\n'
57+
cat "$provider_out"
58+
printf '\n'
59+
if [ "$rc" -eq 0 ]; then
60+
printf 'REVIEW_PASS\n'
61+
else
62+
printf 'REVIEW_FAIL rc=%s\n' "$rc"
63+
fi
64+
printf '<<<END>>>\n'
65+
} > "$RALPHTERM_OUTPUT_FILE"
66+
fi
67+
4568
if [ "$rc" -eq 0 ]; then
4669
printf '\nCOMPLETED\n'
4770
else

0 commit comments

Comments
 (0)