Initial commit: cc-ci autonomous orchestrator
Planning + launch + setup material for the cc-ci Co-op Cloud recipe CI server: plan.md (single source of truth), kickoff/launch supervision, and the Builder/Adversary loop prompts. Secrets (.testenv) and runtime dirs are gitignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+203
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# launch.sh — start and supervise the two cc-ci autonomous loops + a watchdog.
|
||||
#
|
||||
# Model (see plan.md §6 / §6.1): two INDEPENDENT Claude Code sessions —
|
||||
# • Builder (tmux session: cc-ci-builder) working clone /srv/cc-ci/cc-ci
|
||||
# • Adversary (tmux session: cc-ci-adv) working clone /srv/cc-ci/cc-ci-adv
|
||||
# coordinating only through the git repo on git.autonomic.zone.
|
||||
#
|
||||
# Each agent self-paces with a `/loop` (ScheduleWakeup) — that handles ITERATION.
|
||||
# This script's watchdog handles RESILIENCE: it restarts a session that has died
|
||||
# and stops everything once STATUS.md reports "## DONE".
|
||||
#
|
||||
# Usage:
|
||||
# ./launch.sh start # start both loops + watchdog (idempotent)
|
||||
# ./launch.sh watchdog # run only the supervision loop in the foreground
|
||||
# ./launch.sh status # show session + DONE state
|
||||
# ./launch.sh logs builder|adversary|watchdog # tail a session/log
|
||||
# ./launch.sh stop # stop both loops + watchdog
|
||||
#
|
||||
# Configure via env vars (defaults below). At minimum set CC_CI_REPO once the
|
||||
# Builder has created the repo, so the watchdog can detect DONE.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ----- config -------------------------------------------------------------
|
||||
PLAN_DIR="${PLAN_DIR:-/srv/cc-ci/cc-ci-plan}"
|
||||
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
|
||||
# Flags for unattended operation in a sandbox. Override if your setup differs.
|
||||
CLAUDE_FLAGS="${CLAUDE_FLAGS:---dangerously-skip-permissions}"
|
||||
# REMOTE_CONTROL=1 launches each agent as an INTERACTIVE session with --remote-control,
|
||||
# viewable/steerable at claude.ai/code (and the Claude mobile app). This is required for
|
||||
# /loop + ScheduleWakeup to work at all (they are interactive-only — a piped/print-mode
|
||||
# session cannot self-pace). Set REMOTE_CONTROL=0 for a plain interactive session with no
|
||||
# remote surface. The box must be logged into the claude.ai account (run `claude` once to
|
||||
# check `claude auth status`). Each agent gets its own RC session named after its tmux session.
|
||||
REMOTE_CONTROL="${REMOTE_CONTROL:-1}"
|
||||
|
||||
BUILDER_DIR="${BUILDER_DIR:-/srv/cc-ci/cc-ci}" # Builder's repo clone (it creates this)
|
||||
ADV_DIR="${ADV_DIR:-/srv/cc-ci/cc-ci-adv}" # Adversary's repo clone
|
||||
WATCH_DIR="${WATCH_DIR:-/srv/cc-ci/.cc-ci-watch}" # tiny clone the watchdog reads STATUS.md from
|
||||
LOG_DIR="${LOG_DIR:-/srv/cc-ci/.cc-ci-logs}"
|
||||
|
||||
CC_CI_REPO="${CC_CI_REPO:-https://git.autonomic.zone/recipe-maintainers/cc-ci.git}" # CI project repo (DONE detection); harmless until the Builder creates it
|
||||
CC_CI_BRANCH="${CC_CI_BRANCH:-main}"
|
||||
|
||||
WATCH_INTERVAL="${WATCH_INTERVAL:-300}" # seconds between watchdog checks
|
||||
|
||||
BUILDER_SESSION="cc-ci-builder"
|
||||
ADV_SESSION="cc-ci-adv"
|
||||
WATCHDOG_SESSION="cc-ci-watchdog"
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
log() { printf '[launch %(%H:%M:%S)T] %s\n' -1 "$*"; }
|
||||
die() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
need() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
|
||||
|
||||
preflight() {
|
||||
need tmux
|
||||
command -v "$CLAUDE_BIN" >/dev/null 2>&1 || die "claude CLI not found (set CLAUDE_BIN)"
|
||||
[[ -f "$PLAN_DIR/prompts/builder.md" ]] || die "missing $PLAN_DIR/prompts/builder.md"
|
||||
[[ -f "$PLAN_DIR/prompts/adversary.md" ]] || die "missing $PLAN_DIR/prompts/adversary.md"
|
||||
mkdir -p "$LOG_DIR"
|
||||
}
|
||||
|
||||
session_alive() { tmux has-session -t "$1" 2>/dev/null; }
|
||||
|
||||
# Start one agent loop in its own tmux session, cd'd into its working dir, with
|
||||
# the kickoff prompt passed to claude as a positional argument (see below for why
|
||||
# not stdin).
|
||||
start_agent() {
|
||||
local session="$1" workdir="$2" prompt_file="$3"
|
||||
if session_alive "$session"; then
|
||||
log "$session already running — leaving it"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$workdir"
|
||||
log "starting $session (cwd=$workdir, remote_control=$REMOTE_CONTROL)"
|
||||
# tmux gives claude a real PTY, so we run claude INTERACTIVELY (required for /loop +
|
||||
# ScheduleWakeup). The kickoff prompt is passed as a POSITIONAL argument via an inner
|
||||
# `$(cat ...)` — NOT piped on stdin, because piping forces print/headless mode which
|
||||
# breaks both interactivity and --remote-control. The `\$(...)` defers to the inner shell
|
||||
# so the whole multi-line prompt arrives as a single argument.
|
||||
local rc=""
|
||||
[[ "$REMOTE_CONTROL" == "1" ]] && rc="--remote-control '$session'"
|
||||
tmux new-session -d -s "$session" -c "$workdir" \
|
||||
"$CLAUDE_BIN $rc $CLAUDE_FLAGS \"\$(cat '$prompt_file')\""
|
||||
# Log the pane WITHOUT redirecting claude's stdout: a `>>log` redirect makes stdout a
|
||||
# non-tty and drops claude out of interactive/remote-control mode. pipe-pane mirrors the
|
||||
# live pane to the log file while claude keeps the PTY tmux gave it.
|
||||
tmux pipe-pane -o -t "$session" "cat >> '$LOG_DIR/$session.log'"
|
||||
}
|
||||
|
||||
start_loops() {
|
||||
start_agent "$BUILDER_SESSION" "$BUILDER_DIR" "$PLAN_DIR/prompts/builder.md"
|
||||
start_agent "$ADV_SESSION" "$ADV_DIR" "$PLAN_DIR/prompts/adversary.md"
|
||||
}
|
||||
|
||||
# Returns 0 (true) if the repo's STATUS.md contains a "## DONE" heading.
|
||||
is_done() {
|
||||
[[ -n "$CC_CI_REPO" ]] || return 1
|
||||
if [[ ! -d "$WATCH_DIR/.git" ]]; then
|
||||
git clone --depth 1 --branch "$CC_CI_BRANCH" "$CC_CI_REPO" "$WATCH_DIR" >/dev/null 2>&1 || return 1
|
||||
fi
|
||||
git -C "$WATCH_DIR" fetch --depth 1 origin "$CC_CI_BRANCH" >/dev/null 2>&1 || return 1
|
||||
git -C "$WATCH_DIR" reset --hard "origin/$CC_CI_BRANCH" >/dev/null 2>&1 || return 1
|
||||
grep -qE '^##[[:space:]]+DONE' "$WATCH_DIR/STATUS.md" 2>/dev/null
|
||||
}
|
||||
|
||||
watchdog_loop() {
|
||||
log "watchdog up (interval=${WATCH_INTERVAL}s, repo=${CC_CI_REPO:-<unset: DONE-detection disabled>})"
|
||||
while true; do
|
||||
# 1) DONE? then wind everything down.
|
||||
if is_done; then
|
||||
log "STATUS.md reports ## DONE — stopping loops."
|
||||
stop_loops
|
||||
log "watchdog exiting (project complete)."
|
||||
exit 0
|
||||
fi
|
||||
# 2) restart any dead loop (resilience the in-session /loop can't provide).
|
||||
if ! session_alive "$BUILDER_SESSION"; then
|
||||
log "builder session gone — restarting"
|
||||
start_agent "$BUILDER_SESSION" "$BUILDER_DIR" "$PLAN_DIR/prompts/builder.md"
|
||||
fi
|
||||
if ! session_alive "$ADV_SESSION"; then
|
||||
log "adversary session gone — restarting"
|
||||
start_agent "$ADV_SESSION" "$ADV_DIR" "$PLAN_DIR/prompts/adversary.md"
|
||||
fi
|
||||
sleep "$WATCH_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
start_watchdog() {
|
||||
if session_alive "$WATCHDOG_SESSION"; then
|
||||
log "watchdog already running"
|
||||
return 0
|
||||
fi
|
||||
log "starting watchdog"
|
||||
tmux new-session -d -s "$WATCHDOG_SESSION" -c "$PLAN_DIR" \
|
||||
"exec >>'$LOG_DIR/watchdog.log' 2>&1; '$0' watchdog"
|
||||
}
|
||||
|
||||
stop_loops() {
|
||||
for s in "$BUILDER_SESSION" "$ADV_SESSION"; do
|
||||
if session_alive "$s"; then log "killing $s"; tmux kill-session -t "$s" || true; fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
for s in "$BUILDER_SESSION" "$ADV_SESSION" "$WATCHDOG_SESSION"; do
|
||||
if session_alive "$s"; then echo " $s: RUNNING"; else echo " $s: stopped"; fi
|
||||
done
|
||||
if [[ -n "$CC_CI_REPO" ]]; then
|
||||
if is_done; then echo " project: ## DONE"; else echo " project: in progress"; fi
|
||||
else
|
||||
echo " project: (CC_CI_REPO unset — DONE-detection disabled)"
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
start)
|
||||
preflight
|
||||
start_loops
|
||||
start_watchdog
|
||||
log "started. inspect with: ./launch.sh status | attach: tmux attach -t $BUILDER_SESSION"
|
||||
;;
|
||||
watchdog) preflight; watchdog_loop ;;
|
||||
status) cmd_status ;;
|
||||
logs)
|
||||
case "${2:-}" in
|
||||
builder) tail -f "$LOG_DIR/$BUILDER_SESSION.log" ;;
|
||||
adversary) tail -f "$LOG_DIR/$ADV_SESSION.log" ;;
|
||||
watchdog) tail -f "$LOG_DIR/watchdog.log" ;;
|
||||
*) die "usage: $0 logs builder|adversary|watchdog" ;;
|
||||
esac
|
||||
;;
|
||||
stop)
|
||||
stop_loops
|
||||
if session_alive "$WATCHDOG_SESSION"; then log "killing $WATCHDOG_SESSION"; tmux kill-session -t "$WATCHDOG_SESSION" || true; fi
|
||||
log "stopped."
|
||||
;;
|
||||
*)
|
||||
cat <<EOF
|
||||
cc-ci loop launcher
|
||||
|
||||
$0 start start both loops + watchdog (idempotent)
|
||||
$0 status show session + DONE state
|
||||
$0 logs builder|adversary|watchdog tail a log
|
||||
$0 stop stop everything
|
||||
$0 watchdog run supervision loop in foreground
|
||||
|
||||
Key env vars (current value):
|
||||
CC_CI_REPO = ${CC_CI_REPO:-<unset — set to enable DONE detection>}
|
||||
CLAUDE_BIN = $CLAUDE_BIN
|
||||
CLAUDE_FLAGS = $CLAUDE_FLAGS
|
||||
REMOTE_CONTROL = $REMOTE_CONTROL (1 = interactive --remote-control, viewable at claude.ai/code)
|
||||
BUILDER_DIR = $BUILDER_DIR
|
||||
ADV_DIR = $ADV_DIR
|
||||
WATCH_INTERVAL = ${WATCH_INTERVAL}s
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user