diff --git a/cc-ci-plan/launch-assistant.sh b/cc-ci-plan/launch-assistant.sh new file mode 100755 index 0000000..e8a3edb --- /dev/null +++ b/cc-ci-plan/launch-assistant.sh @@ -0,0 +1,102 @@ +#!/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 +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'" + tmux new-session -d -s "$SESSION" -c "$WORKDIR" \ + "$CLAUDE_BIN $sess $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 <