diff --git a/cc-ci-plan/launch-orchestrator.py b/cc-ci-plan/launch-orchestrator.py index 948afc1..3d94b76 100644 --- a/cc-ci-plan/launch-orchestrator.py +++ b/cc-ci-plan/launch-orchestrator.py @@ -78,6 +78,21 @@ def session_alive(): ["tmux", "has-session", "-t", SESSION], capture_output=True ).returncode == 0 +def _ping(session, msg): + """Type a message into the tmux session and submit with Enter.""" + import time + subprocess.run(["tmux", "send-keys", "-t", session, "-l", "--", msg], capture_output=True) + time.sleep(0.5) + for _ in range(5): + subprocess.run(["tmux", "send-keys", "-t", session, "Enter"], capture_output=True) + time.sleep(1) + r = subprocess.run(["tmux", "capture-pane", "-pt", session], + capture_output=True, text=True) + if msg[:28] not in r.stdout.splitlines()[-4:]: + return + subprocess.run(["tmux", "send-keys", "-t", session, "C-m"], capture_output=True) + time.sleep(0.5) + def resume_id(): sid = os.environ.get("ORCH_SESSION_ID") if sid: @@ -118,16 +133,13 @@ def start(mode="resume"): elif BACKEND == "opencode": if not Path(OPENCODE_BIN).exists(): die(f"opencode not found at {OPENCODE_BIN}") - # No --resume equivalent in opencode; STARTUP_PROMPT orients the new session. - # The session title in the web UI identifies it as the orchestrator. - prompt = STARTUP_PROMPT or ( - "You are the cc-ci orchestrator. Read /srv/cc-ci/AGENTS.md and " - "cc-ci-plan/JOURNAL.md for context, then resume supervising the loops." - ) + # Plain `opencode` TUI stays alive in tmux and auto-connects to the shared server. + # (`opencode attach URL` exits without a real TTY; plain TUI works because tmux + # allocates a PTY for the pane.) The startup prompt is sent via ping_session after + # the TUI initialises. NO_COLOR=1 skips the first-run theme picker. cmd = ( f"set -a; . /srv/cc-ci/.testenv; set +a; " - f"{OPENCODE_BIN} {model_flag} run --attach '{OPENCODE_SERVER}' " - f"--title '{SESSION}' '{prompt}'" + f"NO_COLOR=1 {OPENCODE_BIN} {model_flag}" ) log(f"starting {SESSION} (backend=opencode, model={LOOP_MODEL or 'default'})") log(f" visible at http://oc.commoninternet.net (tailnet only)") @@ -139,6 +151,22 @@ def start(mode="resume"): f"cat >> '{LOG_DIR}/{SESSION}.log'"]) log(f"started. attach: tmux attach -t {SESSION}") + # opencode: send the startup prompt once the TUI has connected (~8s). + if BACKEND == "opencode": + import time + time.sleep(8) + prompt = STARTUP_PROMPT or ( + "You are the cc-ci orchestrator. Read cc-ci-plan/JOURNAL.md for full " + "context on what's happened and what needs doing, then supervise the loops." + ) + # Send a pointer to the journal rather than the full prompt (avoids length limits). + bootstrap = ( + "You are the cc-ci orchestrator. Start by reading cc-ci-plan/JOURNAL.md " + "(`cat cc-ci-plan/JOURNAL.md`) for full context, then check loop status " + "(`cc-ci-plan/launch.sh status`) and resume supervising." + ) + _ping(SESSION, bootstrap) + # ── main ────────────────────────────────────────────────────────────────────── def main():