#!/usr/bin/env bash # Control the Claude side of the agent loop on the dev box (no cron/systemd here). # agentctl.sh install copy scripts to ~/.local/share/jyotisha-agent/bin (run from a checkout) # agentctl.sh start start the background loop (tick every 30 min, nightly at 23:30 Beijing) # agentctl.sh stop stop the loop # agentctl.sh status loop pid, last heartbeat, last log lines # agentctl.sh once run one tick now in the foreground # agentctl.sh pause|resume create/remove the STOP file (ticks exit immediately while paused) # The loop runs from the installed copy, because ticks check out and clean the # loop worktree and would otherwise rewrite the scripts under themselves. set -euo pipefail BIN="$HOME/.local/share/jyotisha-agent/bin" CONF_DIR="$HOME/.config/jyotisha-agent" STATE_DIR="${AGENT_LOOP_STATE_DIR:-$HOME/.local/state/jyotisha-agent}" PID_FILE="$STATE_DIR/loop.pid" HEARTBEAT="$STATE_DIR/heartbeat" TICK_SECONDS="${AGENT_LOOP_TICK_SECONDS:-1800}" NIGHTLY_AT="${AGENT_LOOP_NIGHTLY_AT:-23:30}" # Asia/Shanghai mkdir -p "$STATE_DIR" running() { [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; } loop() { echo $$ > "$PID_FILE" while true; do date -u +%FT%TZ > "$HEARTBEAT" "$BIN/claude-tick.sh" || echo "$(date -u +%FT%TZ) tick exited $?" >> "$STATE_DIR/claude-tick.log" today="$(TZ=Asia/Shanghai date +%F)"; now="$(TZ=Asia/Shanghai date +%H:%M)" if [[ ! "$now" < "$NIGHTLY_AT" && "$(cat "$STATE_DIR/nightly.done" 2>/dev/null)" != "$today" ]]; then "$BIN/nightly-review.sh" || true echo "$today" > "$STATE_DIR/nightly.done" fi sleep "$TICK_SECONDS" done } case "${1:-}" in install) src="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" mkdir -p "$BIN" "$CONF_DIR" rm -rf "$BIN.new" && cp -R "$src" "$BIN.new" && rm -rf "$BIN" && mv "$BIN.new" "$BIN" chmod +x "$BIN"/*.sh "$BIN"/hooks/*/pre-push echo "installed to $BIN from $src ($(git -C "$src" rev-parse --short HEAD 2>/dev/null || echo '?'))" [[ -r "$CONF_DIR/gitea-token" ]] && echo "gitea user: $("$BIN/gitea.sh" whoami)" || echo "WARNING: $CONF_DIR/gitea-token missing" ;; start) running && { echo "already running (pid $(cat "$PID_FILE"))"; exit 0; } [[ -x "$BIN/claude-tick.sh" ]] || { echo "run '$0 install' first"; exit 1; } setsid nohup "$BIN/agentctl.sh" __loop >> "$STATE_DIR/loop.out" 2>&1 < /dev/null & sleep 1; running && echo "started (pid $(cat "$PID_FILE"))" || echo "failed to start, see $STATE_DIR/loop.out" ;; __loop) loop ;; stop) if running; then kill -- -"$(ps -o pgid= "$(cat "$PID_FILE")" | tr -d ' ')" 2>/dev/null || kill "$(cat "$PID_FILE")"; rm -f "$PID_FILE"; echo stopped; else echo "not running"; fi ;; status) running && echo "running (pid $(cat "$PID_FILE"))" || echo "NOT running" [[ -e "$CONF_DIR/STOP" ]] && echo "PAUSED (STOP file present)" echo "last heartbeat: $(cat "$HEARTBEAT" 2>/dev/null || echo never)" echo "--- claude-tick.log"; tail -n 8 "$STATE_DIR/claude-tick.log" 2>/dev/null || true echo "--- nightly.log"; tail -n 3 "$STATE_DIR/nightly.log" 2>/dev/null || true ;; once) "$BIN/claude-tick.sh"; tail -n 5 "$STATE_DIR/claude-tick.log" ;; pause) mkdir -p "$CONF_DIR"; touch "$CONF_DIR/STOP"; echo paused ;; resume) rm -f "$CONF_DIR/STOP"; echo resumed ;; *) sed -n '2,9p' "$0"; exit 64 ;; esac