#!/usr/bin/expect -f
# Start Claude Code for an unattended run, answering the first-run gates.
#
# Claude Code has three one-time interactive prompts, and every one of them is a
# silent, permanent hang for an agent with nobody at the keyboard — no error, no
# log line, just a container that looks healthy and does nothing:
#
#   1. the theme picker        (first run, or whenever the installed version is
#                               newer than lastOnboardingVersion)
#   2. "do you trust this folder?"  (per workspace)
#   3. the Bypass Permissions disclaimer  (for --dangerously-skip-permissions)
#
# `seed-claude-config.py` pre-sets the config keys for 1 and 2. The disclaimer
# has no such key — it is meant to be accepted by a person once — so it is
# answered here instead. That is the honest reading of `sylph-agent loose`: the
# operator accepted it by choosing to run this, and the container is exactly the
# sandbox the warning asks for.
#
# ── Why the patterns are single words ──
# Claude Code draws its UI with ABSOLUTE COLUMN escapes between words, so the
# prompt arrives on the wire as
#
#     2.\x1b[8GYes,\x1b[13GI\x1b[15Gaccept
#
# A multi-word pattern like {Yes, I accept} therefore never matches, and the
# wrapper sits there looking like it is not running at all. Match one word.

set timeout 90
log_user 1

# Give the pty a wide, tall geometry. A detached `docker run -t` defaults to
# 80x24, and Claude Code hard-wraps to the terminal width — which truncates the
# Remote Control URL to "https://claude.ai/code/session_01…" in the one place
# you need to read it, and makes `docker logs` nearly unusable generally.
set stty_init "rows 50 cols 200"

set answered_theme 0
set answered_trust 0
set answered_bypass 0

spawn -noecho claude --dangerously-skip-permissions {*}$argv
set child_pid [exp_pid]

# 🔴 THIS WRAPPER USED TO SWALLOW BOTH THE SIGNAL AND THE EXIT STATUS, and those
# two omissions caused most of this project's multi-hour outages. Found
# 2026-09-03 by tracing the signal path, after a tooling review predicted exactly
# this from the symptoms.
#
# The path is:  tini (PID 1) -> entrypoint.sh (exec'd) -> expect -> spawn -> claude
#
# `spawn` CANNOT be an exec: expect has to stay alive to drive the pty. So expect
# is the process Docker signals, and everything below it depends on expect
# passing things along. It did not.
#
# 1. NO SIGNAL FORWARDING. `docker stop` sent SIGTERM to expect, which died and
#    took the pty with it. Claude Code never received a SIGTERM, so it never ran
#    its `SessionEnd` hooks and never wrote `lastSessionId`/`history` to
#    `~/.claude.json` -- which are written only at a GRACEFUL shutdown. That is
#    the whole reason `claude --continue` answered "No conversation found to
#    continue" with 33 MB of transcripts sitting in the volume beside it, and why
#    we resume by scraping a session id off a transcript filename instead.
#
# 2. `eof { exit }` RETURNED 0 FOR EVERY DEATH. A bare `exit` in expect is exit
#    ZERO. So when the kernel OOM-killer took the child, expect saw EOF and
#    reported a clean exit -- `OOMKilled: true` with `ExitCode 0`, which is not
#    Docker being odd, it is this line. It also meant `--restart on-failure`
#    would have treated a memory kill as success, which is why the policy had to
#    be `unless-stopped`.
#
# Both are fixed here. Signals are forwarded to the child and its real status is
# propagated, so a kill reads as 137, a clean stop lets Claude Code shut down
# properly, and the exit code means what it says.
proc forward {sig} {
    global child_pid
    catch { exec kill -$sig $child_pid }
}
trap { forward TERM } SIGTERM
trap { forward INT }  SIGINT
trap { forward HUP }  SIGHUP

expect {
    -re {Choose} {
        if {!$answered_theme} { set answered_theme 1; send "\r" }
        exp_continue
    }
    -re {trust} {
        if {!$answered_trust} {
            set answered_trust 1
            send_user "\n\[claude-autonomous] accepting the workspace trust prompt\n"
            send "1\r"
        }
        exp_continue
    }
    -re {accept} {
        if {!$answered_bypass} {
            set answered_bypass 1
            send_user "\n\[claude-autonomous] accepting the Bypass Permissions disclaimer\n"
            send "2\r"
        }
        exp_continue
    }
    timeout {
        # No new gate for a while: the session is up (or never had one). Stop
        # matching so nothing later in the run can be answered by accident.
    }
    eof { exit }
}

# Hand the terminal over for the rest of the run.
interact

# Propagate the child's REAL exit status. `interact` returns when the child is
# gone; `wait` then yields {pid spawnid os_error status}. Without this the script
# simply ran off the end and returned 0 -- see the note at `spawn` above for what
# that cost.
catch wait result
set status 0
if {[info exists result] && [llength $result] >= 4} {
    # os_error_flag (index 2) is -1 for a normal exit; anything else means the
    # wait itself failed and the status field is not a status.
    if {[lindex $result 2] == 0} {
        set status [lindex $result 3]
    }
}
exit $status
