#!/usr/bin/env bash # # launch-assistant.sh — start/resume the cc-ci ASSISTANT session in tmux under remote-control. # # The Assistant is a general-purpose, remote-controllable Claude session that shares the cc-ci # workspace + access with the orchestrator, but is NOT on a loop. It sits idle until the orchestrator # (or the operator) hands it a plan/task; it does the task against the workspace, reports back, and # waits for the next one. Modelled on launch-orchestrator.sh. # # Naming: tmux session AND remote-control name are both "cc-ci-assistant" (matching # cc-ci-orch / cc-ci-builder / cc-ci-adv / cc-ci-watchdog). # # Usage: # ./launch-assistant.sh start # resume the persistent assistant session (DEFAULT); creates it on first run # ./launch-assistant.sh fresh # start a NEW assistant session (new id) # ./launch-assistant.sh status # show tmux + remote-control state # ./launch-assistant.sh attach # tmux attach (Ctrl-b d to detach) # ./launch-assistant.sh stop # kill the tmux session (conversation persists on disk) set -euo pipefail SESSION="${ASSISTANT_SESSION:-cc-ci-assistant}" # tmux session name == remote-control name WORKDIR="${ASSISTANT_DIR:-/srv/cc-ci}" # same workspace as the orchestrator CLAUDE_BIN="${CLAUDE_BIN:-/home/loops/.local/bin/claude}" # --dangerously-skip-permissions is blocked for root (use the env var there); as a non-root user the # flag works. Mirror launch.sh's detection. if [ "$(id -u)" = "0" ]; then export CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1; CLAUDE_FLAGS="${CLAUDE_FLAGS:-}"; else CLAUDE_FLAGS="${CLAUDE_FLAGS:---dangerously-skip-permissions}"; fi ASSISTANT_MODEL="${ASSISTANT_MODEL:-sonnet}" # the assistant runs on sonnet (cheaper for task work) REMOTE_CONTROL="${REMOTE_CONTROL:-1}" # 1 => --remote-control (viewable at claude.ai/code) LOG_DIR="${LOG_DIR:-/srv/cc-ci/.cc-ci-logs}" ID_FILE="${ASSISTANT_ID_FILE:-$LOG_DIR/.assistant-session-id}" # Startup brief: defines the assistant's role. Injected as the session's first/next turn. No single quotes. STARTUP_PROMPT="${ASSISTANT_STARTUP_PROMPT-You are the cc-ci ASSISTANT — a general-purpose helper sharing the cc-ci workspace (/srv/cc-ci) and access (ssh cc-ci, .testenv, the plan files) with the orchestrator. You are NOT on a loop and you do NOT supervise the Builder/Adversary loops. Sit idle until the orchestrator or operator hands you a specific plan or task; then do it carefully, report the result, and wait for the next one. Respect single-writer discipline: the loops own the cc-ci product-repo clones (/srv/cc-ci/cc-ci, /srv/cc-ci/cc-ci-adv) — do not edit those unless a task explicitly says to. If you have just (re)launched and have no pending task, briefly confirm you are online and idle, then wait.}" log() { printf '[assistant %(%H:%M:%S)T] %s\n' -1 "$*"; } die() { log "ERROR: $*"; exit 1; } session_alive() { tmux has-session -t "$SESSION" 2>/dev/null; } preflight() { command -v tmux >/dev/null 2>&1 || die "missing dependency: tmux" command -v "$CLAUDE_BIN" >/dev/null 2>&1 || die "claude CLI not found at $CLAUDE_BIN (set CLAUDE_BIN)" [[ -d "$WORKDIR" ]] || die "workdir not found: $WORKDIR" mkdir -p "$LOG_DIR" # seed a stable session id on first ever launch [[ -f "$ID_FILE" ]] || cat /proc/sys/kernel/random/uuid > "$ID_FILE" } sid() { cat "$ID_FILE" 2>/dev/null; } # does a transcript already exist for this id? (project dir derived from WORKDIR, e.g. -srv-cc-ci) have_transcript() { local key; key="$(printf '%s' "$WORKDIR" | sed 's#/#-#g')" [[ -f "$HOME/.claude/projects/$key/$(sid).jsonl" ]] } # $1 = resume|fresh start() { local mode="${1:-resume}" preflight if session_alive; then log "$SESSION already running — leaving it (use '$0 stop' first to relaunch)"; return 0 fi local rc="" sess="" id; id="$(sid)" [[ "$REMOTE_CONTROL" == "1" ]] && rc="--remote-control '$SESSION'" if [[ "$mode" == "fresh" ]]; then id="$(cat /proc/sys/kernel/random/uuid)"; echo "$id" > "$ID_FILE" sess="--session-id '$id'"; log "starting $SESSION FRESH (new id=$id)" elif have_transcript; then sess="--resume '$id'"; log "starting $SESSION (resume id=$id)" else sess="--session-id '$id'"; log "starting $SESSION (first run, pin id=$id)" fi local prompt_arg="" [[ -n "$STARTUP_PROMPT" ]] && prompt_arg="'$STARTUP_PROMPT'" local model=""; [[ -n "$ASSISTANT_MODEL" ]] && model="--model '$ASSISTANT_MODEL'" tmux new-session -d -s "$SESSION" -c "$WORKDIR" \ "$CLAUDE_BIN $sess $model $rc $CLAUDE_FLAGS $prompt_arg" tmux pipe-pane -o -t "$SESSION" "cat >> '$LOG_DIR/$SESSION.log'" log "started. status: $0 status | attach: tmux attach -t $SESSION | id: $id" } case "${1:-start}" in start) start resume ;; fresh) start fresh ;; stop) if session_alive; then log "killing $SESSION"; tmux kill-session -t "$SESSION" || true; else log "$SESSION not running"; fi ;; status) if session_alive; then log "$SESSION: RUNNING"; ps -eo pid,etime,args | grep "[r]emote-control $SESSION" || true else log "$SESSION: stopped"; fi log "session id: $(sid) (file: $ID_FILE)" ;; attach) exec tmux attach -t "$SESSION" ;; *) cat <