refactor: rewrite launchers as Python; add orchestrator JOURNAL.md

Bash scripts are now one-liner wrappers: exec python3 <script>.py "$@"
All logic lives in the Python scripts (pure stdlib, no deps).

launch.py — loops + watchdog:
  Full port of launch.sh: phase sequencing, start/stop/status/logs/watchdog,
  handoff signalling, stall detection, heal_session, heal_orchestrator.
  Cleaner structure: config block → helpers → phase/kickoff/agent/healing/
  handoff/watchdog/main. LOOP_BACKEND + LOOP_MODEL switches throughout.

launch-orchestrator.py — orchestrator session:
  claude path: --resume <id> preserved (conversation survives reboots).
  opencode path: run --attach --title (no --resume; STARTUP_PROMPT orients
  the new session; reads JOURNAL.md for context).
  STARTUP_PROMPT updated to reference JOURNAL.md on startup.

launch-upgrader.py — one-shot upgrade job:
  LOOP_BACKEND / LOOP_MODEL take precedence over UPGRADER_BACKEND / UPGRADER_MODEL.
  Both claude and opencode paths supported.

cc-ci-plan/JOURNAL.md — new orchestrator handoff file:
  Persistent across conversation resets. Documents the handoff format and
  carries the current session's summary: migration complete, phase 5 in
  progress (V3/V7 PASS), phase 4 deferred, open items for next session.

AGENTS.md: step 1 on startup = read JOURNAL.md; step 5 = append on handoff.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
autonomic-bot
2026-05-31 17:50:09 +00:00
co-authored by Claude Sonnet 4.6
parent e0e5bf6e64
commit bca51071bd
8 changed files with 1067 additions and 781 deletions
+2 -150
View File
@@ -1,151 +1,3 @@
#!/usr/bin/env bash
#
# launch-upgrader.sh — spin up the cc-ci UPGRADER agent in tmux under remote-control.
#
# The Upgrader is a ONE-SHOT job agent (not a perpetual loop like the Builder/Adversary): it runs the
# weekly recipe-upgrade sequence — the /upgrade-all skill in DEFAULT mode — to completion, then STOPS
# and stays idle (it does NOT self-terminate) so the run + summary remain viewable/steerable at
# claude.ai/code exactly like the Builder, instead of being buried in headless cron output. The next
# weekly run starts a fresh session: `start` leaves an in-flight run alone but clears a finished/idle
# (or wedged) session and starts clean. The weekly cron (Sat 03:00 UTC, once cc-ci is built — see
# [[cc-ci-upgrade-all-cron]]) invokes `launch-upgrader.sh start`.
#
# Naming: tmux session AND remote-control name are both "cc-ci-upgrader" (matching
# cc-ci-builder / cc-ci-adv / cc-ci-watchdog / cc-ci-orchestrator).
#
# Usage:
# ./launch-upgrader.sh start # use-or-create: if a run is actively in flight leave it,
# # else (no session / idle-stale) kill any stale + start fresh
# ./launch-upgrader.sh fresh # always kill any existing + start a fresh run
# ./launch-upgrader.sh status | attach | stop
#
# Env:
# UPGRADER_ARGS="" passthrough args to /upgrade-all (e.g. "--dry-run", "ghost n8n"); default none
# = full default fleet run. NEVER pass --with-tests here (the cron must not
# auto-edit tests; that's the operator's per-recipe opt-in).
set -euo pipefail
SESSION="${UPGRADER_SESSION:-cc-ci-upgrader}" # tmux session name == remote-control name
WORKDIR="${UPGRADER_DIR:-/srv/cc-ci}" # cwd: where .claude/skills/ + .testenv live
# Backend selection — mirrors launch.sh. LOOP_BACKEND overrides for consistency.
UPGRADER_BACKEND="${LOOP_BACKEND:-${UPGRADER_BACKEND:-claude}}" # "claude" or "opencode"
# Model: LOOP_MODEL > UPGRADER_MODEL > backend default (sonnet for claude, provider/model for opencode).
UPGRADER_MODEL="${LOOP_MODEL:-${UPGRADER_MODEL:-sonnet}}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
CLAUDE_FLAGS="${CLAUDE_FLAGS:---dangerously-skip-permissions}"
OPENCODE_BIN="${OPENCODE_BIN:-/home/loops/.local/bin/opencode}"
OPENCODE_SERVER="${OPENCODE_SERVER:-http://127.0.0.1:4096}"
REMOTE_CONTROL="${REMOTE_CONTROL:-1}" # 1 => --remote-control / opencode web
LOG_DIR="${LOG_DIR:-/srv/cc-ci/.cc-ci-logs}"
UPGRADER_ARGS="${UPGRADER_ARGS:-}"
log() { printf '[upgrader %(%H:%M:%S)T] %s\n' -1 "$*"; }
die() { log "ERROR: $*"; exit 1; }
session_alive() { tmux has-session -t "$SESSION" 2>/dev/null; }
# "actively working" = claude shows interrupt hint; opencode shows spinner/Running tool.
session_busy() { tmux capture-pane -pt "$SESSION" 2>/dev/null | grep -qE 'esc to interrupt|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏|Running tool'; }
preflight() {
command -v tmux >/dev/null 2>&1 || die "missing dependency: tmux"
case "$UPGRADER_BACKEND" in
claude) command -v "$CLAUDE_BIN" >/dev/null 2>&1 || die "claude CLI not found (set CLAUDE_BIN)" ;;
opencode) command -v "$OPENCODE_BIN" >/dev/null 2>&1 || die "opencode not found (set OPENCODE_BIN)"
[[ -n "$OPENCODE_HOST" ]] || die "could not detect tailscale IP for OPENCODE_HOST" ;;
*) die "unknown UPGRADER_BACKEND '$UPGRADER_BACKEND' — use 'claude' or 'opencode'" ;;
esac
[[ -d "$WORKDIR" ]] || die "workdir not found: $WORKDIR"
[[ -d "$WORKDIR/.claude/skills/upgrade-all" ]] || die "upgrade-all skill not found under $WORKDIR/.claude/skills"
mkdir -p "$LOG_DIR"
}
write_kickoff() {
local kf="$LOG_DIR/.kickoff-$SESSION.txt"
cat > "$kf" <<KICK
*** cc-ci UPGRADER — weekly recipe-upgrade job ***
You are the cc-ci Upgrader: a ONE-SHOT job agent, NOT a perpetual loop. Run the recipe-upgrade
sequence to completion, then STOP. Your cwd is ${WORKDIR}; reach the CI server with \`ssh cc-ci\`;
creds are in ${WORKDIR}/.testenv; the skills live in ${WORKDIR}/.claude/skills/.
DO THIS:
1. Invoke the **/upgrade-all** skill in DEFAULT mode${UPGRADER_ARGS:+ with arguments: ${UPGRADER_ARGS}}
(read ${WORKDIR}/.claude/skills/upgrade-all/SKILL.md for the full procedure). It surveys every
enrolled recipe and, for each upgradeable one, runs /recipe-upgrade in DEFAULT mode — recipe PR
only, verified by posting \`!testme\` on the PR (results visible in the PR, iterate up to 3x). A
genuinely stale test gets an explanatory PR COMMENT, never a test edit.
2. Process recipes via per-recipe SUBAGENTS (as the skill specifies) so your own context stays light.
If your context usage climbs (~80%), run /compact before continuing.
3. Write + push the weekly summary (the PR list is the actionable output for the operator).
4. WHEN THE RUN IS COMPLETE: STOP. Print the final summary (lead with the PR list) and an
\`UPGRADE RUN COMPLETE\` line, then go idle. Do NOT loop, do NOT re-run, and do NOT kill your own
session — leave it up so the operator can review your output + the summary in the web UI
(claude.ai/code). Next week's run starts a fresh session (the launcher clears this idle one).
GUARDRAILS: NEVER merge any PR. NEVER weaken a test. DEFAULT mode only — do NOT pass --with-tests
(updating cc-ci tests is the operator's per-recipe opt-in). Single-writer: dedicated branches +
separate clones, never push main, never touch the build loops' /cc-ci /cc-ci-adv clones. The shared
Swarm is stateful — go sequentially and tear down what you deploy.
KICK
echo "$kf"
}
start() {
local mode="${1:-use-or-create}"
preflight
if session_alive; then
if [[ "$mode" == "use-or-create" ]] && session_busy; then
log "$SESSION already running a job (busy) — leaving it"; return 0
fi
log "$SESSION exists but idle/stale (or fresh requested) — killing it first"
tmux kill-session -t "$SESSION" 2>/dev/null || true; sleep 1
fi
local kf
kf="$(write_kickoff)"
log "starting $SESSION (backend=$UPGRADER_BACKEND, model=$UPGRADER_MODEL, args='${UPGRADER_ARGS:-<none>}')"
case "$UPGRADER_BACKEND" in
claude)
local rc=""
[[ "$REMOTE_CONTROL" == "1" ]] && rc="--remote-control '$SESSION'"
tmux new-session -d -s "$SESSION" -c "$WORKDIR" \
"$CLAUDE_BIN $rc --model '$UPGRADER_MODEL' $CLAUDE_FLAGS \"\$(cat '$kf')\""
;;
opencode)
tmux new-session -d -s "$SESSION" -c "$WORKDIR" \
"set -a; . /srv/cc-ci/.testenv; set +a; $OPENCODE_BIN --model '$UPGRADER_MODEL' run --attach '$OPENCODE_SERVER' --title '$SESSION' \"\$(cat '$kf')\""
log "$SESSION visible in web UI at http://oc.commoninternet.net (tailnet only)"
;;
esac
tmux pipe-pane -o -t "$SESSION" "cat >> '$LOG_DIR/$SESSION.log'"
log "started. status: $0 status | attach: tmux attach -t $SESSION | log: $LOG_DIR/$SESSION.log"
}
case "${1:-start}" in
start) start use-or-create ;;
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 $(session_busy && echo '(busy)' || echo '(idle/finishing)')"
ps -eo pid,etime,args | grep "[r]emote-control $SESSION" || true
else log "$SESSION: stopped"; fi ;;
attach) exec tmux attach -t "$SESSION" ;;
*)
cat <<EOF
cc-ci upgrader launcher — one-shot weekly recipe-upgrade job agent (remote-control)
$0 start use-or-create: leave an in-flight run alone, else (re)start fresh (DEFAULT; what the cron calls)
$0 fresh always kill any existing + start a fresh run
$0 status show tmux + remote-control state
$0 attach tmux attach to the session
$0 stop kill the session
Env: UPGRADER_BACKEND=$UPGRADER_BACKEND UPGRADER_MODEL=$UPGRADER_MODEL UPGRADER_ARGS='${UPGRADER_ARGS:-<none>}'
claude: CLAUDE_BIN=$CLAUDE_BIN REMOTE_CONTROL=$REMOTE_CONTROL
opencode: OPENCODE_BIN=$OPENCODE_BIN OPENCODE_SERVER=$OPENCODE_SERVER web=http://oc.commoninternet.net
(LOOP_BACKEND / LOOP_MODEL override UPGRADER_BACKEND / UPGRADER_MODEL for unified control)
The agent runs /upgrade-all (DEFAULT mode) to completion, then STOPS and stays idle (viewable in the
web UI). It does NOT self-terminate; the next weekly `start` clears the idle session and runs fresh.
EOF
;;
esac
# Thin wrapper — delegates everything to launch-upgrader.py in the same directory.
exec python3 "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/launch-upgrader.py" "$@"