Explorar o código

feat(skills): fleetflow ff-clean — autoclean lanes + cache (feature 8)

ff-clean.sh --run NAME [--repo P] [--force] reclaims a run's lanes:
- zero-commit CLEAN lane     -> git worktree remove + branch -D   ("removed")
- zero-commit DIRTY lane     -> "kept" unless --force              (--force removes)
- N>0 commit lane            -> "kept (N commits)"                 (never auto-removed)
- locked ACL-litter dir      -> "locked (needs elevation)"         (reported, skipped)

Then removes each lane's cache/tmp dir under FLEETFLOW_CACHE_ROOT
(<root>/<run>-<id>). Committed lanes are never force-removed (their work would
be lost); a locked lane never aborts the run. Pass the SAME FLEETFLOW_CACHE_ROOT
used at spawn time so ff-clean can find the redirected cache dirs. stdout is one
TSV line per lane (id<TAB>status<TAB>detail); chatter on stderr; exit 0 on
completion, 2 on usage/no-such-run.

Co-Authored-By: Claude <noreply@anthropic.com>
0xDarkMatter hai 2 semanas
pai
achega
b807ceb1e5
Modificáronse 1 ficheiros con 135 adicións e 0 borrados
  1. 135 0
      skills/fleetflow/scripts/ff-clean.sh

+ 135 - 0
skills/fleetflow/scripts/ff-clean.sh

@@ -0,0 +1,135 @@
+#!/usr/bin/env bash
+# ff-clean.sh - reclaim a fleetflow run's lanes, branches, and cache dirs.
+#
+# Per lane (worktree lane under <run>/wt-<id>):
+#   zero commits + clean      -> git worktree remove + branch -D   ("removed")
+#   zero commits + dirty      -> "kept" unless --force              (--force removes)
+#   N>0 commits               -> "kept (N commits)"                 (never auto-remove)
+#   locked ACL-litter dir     -> "locked (needs elevation)"         (continue, non-fatal)
+# Then removes each lane's cache dir under FLEETFLOW_CACHE_ROOT (default
+# $HOME/.fleet-worker/cache/<run>-<id>). Removal never aborts the run: a locked
+# lane is reported and skipped, and the script exits 0 once it has tried them all.
+# stdout: one TSV line per lane: id<TAB>status<TAB>detail. stderr: chatter.
+#
+# Exit codes: 0 done (incl. some locked lanes) | 2 usage / no such run
+set -u
+
+FF_VERSION="1.1.0"
+
+usage() {
+  cat <<'EOF'
+Usage: ff-clean.sh --run NAME [--repo PATH] [--force]
+
+  --run NAME      run name under <repo>/.fleetflow/
+  --repo PATH     repo root (default: git toplevel of cwd)
+  --force         also remove DIRTY zero-commit lanes (committed lanes are
+                  ALWAYS kept - land or branch them first, then clean)
+
+  Lanes with commits are never removed (their work would be lost); clean only
+  after ff-collect has gated them and the orchestrator has landed/branched the
+  keepers. Locked codex sandbox-litter dirs are reported and skipped.
+
+EXAMPLES
+  ff-clean.sh --run currency
+  ff-clean.sh --run currency --force
+  ff-clean.sh --run currency --repo /path/to/repo | column -t
+EOF
+}
+
+err() { echo "ff-clean: $*" >&2; }
+emit() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }   # id <TAB> status <TAB> detail
+
+RUN="" REPO="" FORCE=0
+while [ $# -gt 0 ]; do
+  case "$1" in
+    --run) RUN="${2:-}"; shift 2 ;;
+    --repo) REPO="${2:-}"; shift 2 ;;
+    --force) FORCE=1; shift ;;
+    -h|--help) usage; exit 0 ;;
+    *) err "unknown argument: $1"; usage >&2; exit 2 ;;
+  esac
+done
+
+command -v jq >/dev/null || { err "jq required"; exit 2; }
+command -v git >/dev/null || { err "git required"; exit 2; }
+[ -n "$RUN" ] || { err "--run required"; usage >&2; exit 2; }
+[ -n "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)" || true
+[ -n "$REPO" ] && [ -d "$REPO" ] || { err "not in a git repo (or --repo invalid)"; exit 2; }
+
+RUNDIR="$REPO/.fleetflow/$RUN"
+MANIFEST="$RUNDIR/manifest.json"
+[ -d "$RUNDIR" ] || { err "no such run: $RUNDIR"; exit 2; }
+
+CACHE_ROOT="${FLEETFLOW_CACHE_ROOT:-$HOME/.fleet-worker/cache}"
+BASE="$(jq -r '.base // "main"' "$MANIFEST" 2>/dev/null)"; [ -n "$BASE" ] || BASE="main"
+
+# enumerate lane ids: manifest packets (authoritative) + any wt-* dirs left on
+# disk (e.g. manifest pruned but lanes remain) + journal started ids (fallback).
+list_lane_ids() {
+  local ids=""
+  [ -f "$MANIFEST" ] && ids="$(jq -r '.packets[].id' "$MANIFEST" 2>/dev/null)"
+  local d
+  for d in "$RUNDIR"/wt-*; do
+    [ -d "$d" ] || continue
+    ids="$ids
+$(basename "$d" | sed 's/^wt-//')"
+  done
+  if [ -z "$ids" ] && [ -f "$RUNDIR/journal.jsonl" ]; then
+    ids="$(jq -r 'select(.type=="started") | .id' "$RUNDIR/journal.jsonl" 2>/dev/null)"
+  fi
+  printf '%s\n' "$ids" | awk 'NF && !seen[$0]++'
+}
+
+# remove_lane <id> <force>: worktree remove (+ branch -D on success). Reports
+# "locked" if the OS refuses to delete (codex AppContainer-ACL litter).
+remove_lane() {
+  local id="$1" f="$2" wt="$RUNDIR/wt-$id" br="fleetflow/$RUN/$id" wflag=""
+  [ "$f" = 1 ] && wflag="--force"
+  if git -C "$REPO" worktree remove $wflag "$wt" 2>>"$RUNDIR/$id.clean.err"; then
+    git -C "$REPO" branch -D "$br" >/dev/null 2>>"$RUNDIR/$id.clean.err" \
+      && emit "$id" removed "worktree + branch gone" \
+      || emit "$id" removed "worktree gone (branch delete skipped)"
+  else
+    emit "$id" locked "needs elevation (see $RUNDIR/$id.clean.err)"
+  fi
+}
+
+# clean_cache <id>: best-effort removal of the lane's cache/tmp dir.
+clean_cache() {
+  local id="$1" c="$CACHE_ROOT/$RUN-$id"
+  [ -d "$c" ] || return 0
+  if rm -rf "$c" 2>>"$RUNDIR/$id.clean.err"; then err "cache removed: $c"
+  else err "cache locked (needs elevation): $c"; fi
+}
+
+NLANES="$(list_lane_ids | wc -l | tr -d ' ')"
+err "cleaning run '$RUN': $NLANES lane(s); FORCE=$FORCE; base=$BASE; cache=$CACHE_ROOT"
+if [ "$NLANES" = "0" ]; then
+  err "no lanes to clean"
+  exit 0
+fi
+
+# base ref must exist for rev-list counting; fall back to HEAD once for the run
+git -C "$REPO" show-ref --verify --quiet "refs/heads/$BASE" || BASE="HEAD"
+
+while read -r id; do
+  [ -n "$id" ] || continue
+  wt="$RUNDIR/wt-$id"
+  if [ -d "$wt" ]; then
+    commits="$(git -C "$wt" rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0)"
+    dirt="$(git -C "$wt" status --porcelain 2>/dev/null)"
+    if [ "${commits:-0}" -eq 0 ] && [ -z "$dirt" ]; then
+      remove_lane "$id" 0
+    elif [ "${commits:-0}" -eq 0 ] && [ -n "$dirt" ]; then
+      if [ "$FORCE" = 1 ]; then remove_lane "$id" 1
+      else emit "$id" kept "dirty, zero commits (use --force)"; fi
+    else
+      emit "$id" kept "$commits commits"
+    fi
+  else
+    emit "$id" kept "no worktree lane"
+  fi
+  clean_cache "$id"
+done < <(list_lane_ids)
+
+exit 0