diff --git a/bin/auto-commit-guard.sh b/bin/auto-commit-guard.sh new file mode 100644 index 00000000..e95494d7 --- /dev/null +++ b/bin/auto-commit-guard.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# auto-commit-guard.sh — Background daemon that auto-commits uncommitted work +# +# Usage: auto-commit-guard.sh [interval_seconds] [worktree_base] +# auto-commit-guard.sh # defaults: 120s, ~/worktrees +# auto-commit-guard.sh 60 # check every 60s +# auto-commit-guard.sh 180 ~/my-worktrees +# +# Scans all git repos under the worktree base for uncommitted changes. +# If dirty for >= 1 check cycle, auto-commits with a WIP message. +# Pushes unpushed commits so work is always recoverable from the remote. +# +# Also scans /tmp for orphaned agent workdirs on startup. + +set -uo pipefail + +INTERVAL="${1:-120}" +WORKTREE_BASE="${2:-$HOME/worktrees}" +LOG_DIR="$HOME/.hermes/logs" +LOG="$LOG_DIR/auto-commit-guard.log" +PIDFILE="$LOG_DIR/auto-commit-guard.pid" +ORPHAN_SCAN_DONE="$LOG_DIR/.orphan-scan-done" + +mkdir -p "$LOG_DIR" + +# Single instance guard +if [ -f "$PIDFILE" ]; then + old_pid=$(cat "$PIDFILE") + if kill -0 "$old_pid" 2>/dev/null; then + echo "auto-commit-guard already running (PID $old_pid)" >&2 + exit 0 + fi +fi +echo $$ > "$PIDFILE" +trap 'rm -f "$PIDFILE"' EXIT + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] AUTO-COMMIT: $*" >> "$LOG" +} + +# --- Orphaned workdir scan (runs once on startup) --- +scan_orphans() { + if [ -f "$ORPHAN_SCAN_DONE" ]; then + return 0 + fi + log "Scanning /tmp for orphaned agent workdirs..." + local found=0 + local rescued=0 + + for dir in /tmp/*-work-* /tmp/timmy-burn-* /tmp/tc-burn; do + [ -d "$dir" ] || continue + [ -d "$dir/.git" ] || continue + + found=$((found + 1)) + cd "$dir" 2>/dev/null || continue + + local dirty + dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ") + if [ "${dirty:-0}" -gt 0 ]; then + local branch + branch=$(git branch --show-current 2>/dev/null || echo "orphan") + git add -A 2>/dev/null + if git commit -m "WIP: orphan rescue — $dirty file(s) auto-committed on $(date -u +%Y-%m-%dT%H:%M:%SZ) + +Orphaned workdir detected at $dir. +Branch: $branch +Rescued by auto-commit-guard on startup." 2>/dev/null; then + rescued=$((rescued + 1)) + log "RESCUED: $dir ($dirty files on branch $branch)" + + # Try to push if remote exists + if git remote get-url origin >/dev/null 2>&1; then + git push -u origin "$branch" 2>/dev/null && log "PUSHED orphan rescue: $dir → $branch" || log "PUSH FAILED orphan rescue: $dir (no remote access)" + fi + fi + fi + done + + log "Orphan scan complete: $found workdirs checked, $rescued rescued" + touch "$ORPHAN_SCAN_DONE" +} + +# --- Main guard loop --- +guard_cycle() { + local committed=0 + local scanned=0 + + # Scan worktree base + if [ -d "$WORKTREE_BASE" ]; then + for dir in "$WORKTREE_BASE"/*/; do + [ -d "$dir" ] || continue + [ -d "$dir/.git" ] || continue + + scanned=$((scanned + 1)) + cd "$dir" 2>/dev/null || continue + + local dirty + dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ") + [ "${dirty:-0}" -eq 0 ] && continue + + local branch + branch=$(git branch --show-current 2>/dev/null || echo "detached") + + git add -A 2>/dev/null + if git commit -m "WIP: auto-commit — $dirty file(s) on $branch + +Automated commit by auto-commit-guard at $(date -u +%Y-%m-%dT%H:%M:%SZ). +Work preserved to prevent loss on crash." 2>/dev/null; then + committed=$((committed + 1)) + log "COMMITTED: $dir ($dirty files, branch $branch)" + + # Push to preserve remotely + if git remote get-url origin >/dev/null 2>&1; then + git push -u origin "$branch" 2>/dev/null && log "PUSHED: $dir → $branch" || log "PUSH FAILED: $dir (will retry next cycle)" + fi + fi + done + fi + + # Also scan /tmp for agent workdirs + for dir in /tmp/*-work-*; do + [ -d "$dir" ] || continue + [ -d "$dir/.git" ] || continue + + scanned=$((scanned + 1)) + cd "$dir" 2>/dev/null || continue + + local dirty + dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ") + [ "${dirty:-0}" -eq 0 ] && continue + + local branch + branch=$(git branch --show-current 2>/dev/null || echo "detached") + + git add -A 2>/dev/null + if git commit -m "WIP: auto-commit — $dirty file(s) on $branch + +Automated commit by auto-commit-guard at $(date -u +%Y-%m-%dT%H:%M:%SZ). +Agent workdir preserved to prevent loss." 2>/dev/null; then + committed=$((committed + 1)) + log "COMMITTED: $dir ($dirty files, branch $branch)" + + if git remote get-url origin >/dev/null 2>&1; then + git push -u origin "$branch" 2>/dev/null && log "PUSHED: $dir → $branch" || log "PUSH FAILED: $dir (will retry next cycle)" + fi + fi + done + + [ "$committed" -gt 0 ] && log "Cycle done: $scanned scanned, $committed committed" +} + +# --- Entry point --- +log "Starting auto-commit-guard (interval=${INTERVAL}s, worktree=${WORKTREE_BASE})" +scan_orphans + +while true; do + guard_cycle + sleep "$INTERVAL" +done