cancel

Cleanly exit any active persistent mode (ralph, ultrawork, autopilot, ralplan, team, swarm) and clean up all state files

Input: $ARGUMENTS — pass --force to delete state files even when no active mode is detected (recovery mode for corrupt/orphaned state).

Cancel any active persistent mode

This command is the clean exit point for any dog_stack persistence loop: /dog:ralph, /dog:autopilot, /dog:ultrawork, /dog:ralplan, /dog:team, or swarm mode. The stop hook in hooks/persistent-mode.cjs reads state files under .dog/state/ and re-injects continuation prompts whenever a mode is marked active: true. This command removes those state files so the loop terminates naturally on the next Stop hook evaluation.

Mode state files managed

Each persistent mode writes to its own JSON file under .dog/state/sessions/<session-id>/ (session-scoped) with fallback to .dog/state/ (legacy unscoped):

ModeState fileEntry point
Ralphralph-state.json/dog:ralph
Ultraworkultrawork-state.json/dog:ultrawork
Autopilotautopilot-state.json/dog:autopilot
Ralplanralplan-state.jsonPRP plan execution
Teamteam-state.json/dog:team
OMC Teamsomc-teams-state.json(OMC compat)
Swarmswarm-summary.json + swarm-active.markerteam swarm mode

Execute these steps

Run the following single bash block. It uses a bash array for path safety (paths may contain spaces), falls back to grep/sed when jq is unavailable, and reports a clean summary at the end.

#!/usr/bin/env bash
set -u

STATE_DIR="$(pwd)/.dog/state"
FORCE=0
case "$ARGUMENTS" in *--force*) FORCE=1 ;; esac

declare -a FOUND=()

# Detect state files across legacy unscoped path + all session-scoped paths
STATE_FILES=(
  "ralph-state.json"
  "ultrawork-state.json"
  "autopilot-state.json"
  "ralplan-state.json"
  "team-state.json"
  "omc-teams-state.json"
  "swarm-summary.json"
)

for name in "${STATE_FILES[@]}"; do
  # Legacy unscoped path
  [ -f "$STATE_DIR/$name" ] && FOUND+=("$STATE_DIR/$name")
  # All session-scoped paths
  if [ -d "$STATE_DIR/sessions" ]; then
    while IFS= read -r -d '' path; do
      FOUND+=("$path")
    done < <(find "$STATE_DIR/sessions" -mindepth 2 -maxdepth 2 -type f -name "$name" -print0 2>/dev/null)
  fi
done

# Swarm active marker (separate file, not JSON)
[ -f "$STATE_DIR/swarm-active.marker" ] && FOUND+=("$STATE_DIR/swarm-active.marker")
if [ -d "$STATE_DIR/sessions" ]; then
  while IFS= read -r -d '' marker; do
    FOUND+=("$marker")
  done < <(find "$STATE_DIR/sessions" -mindepth 2 -maxdepth 2 -type f -name "swarm-active.marker" -print0 2>/dev/null)
fi

# Handle empty case
if [ "${#FOUND[@]}" -eq 0 ]; then
  if [ "$FORCE" -eq 0 ]; then
    echo "No active persistent mode detected. Nothing to cancel."
    exit 0
  fi
  echo "No active state files. Proceeding with --force cleanup of orphaned session dirs..."
fi

# Report per-file info before deletion
if [ "${#FOUND[@]}" -gt 0 ]; then
  echo "Detected ${#FOUND[@]} active state file(s):"
  for f in "${FOUND[@]}"; do
    case "$f" in
      *.marker) echo "  - swarm (active marker: $f)"; continue ;;
    esac
    base="$(basename "$f")"
    mode="${base%-state.json}"
    iter="n/a"; maxiter="n/a"; started="n/a"; active="?"
    if command -v jq >/dev/null 2>&1; then
      iter=$(jq -r '.iteration // "n/a"' "$f" 2>/dev/null)
      maxiter=$(jq -r '.max_iterations // "n/a"' "$f" 2>/dev/null)
      started=$(jq -r '.started_at // "n/a"' "$f" 2>/dev/null)
      active=$(jq -r '.active // "?"' "$f" 2>/dev/null)
    else
      # Fallback: grep the JSON manually for simple scalar fields
      iter=$(grep -oE '"iteration"[[:space:]]*:[[:space:]]*[0-9]+' "$f" | head -1 | grep -oE '[0-9]+$' || echo "n/a")
      maxiter=$(grep -oE '"max_iterations"[[:space:]]*:[[:space:]]*[0-9]+' "$f" | head -1 | grep -oE '[0-9]+$' || echo "n/a")
      started=$(grep -oE '"started_at"[[:space:]]*:[[:space:]]*"[^"]*"' "$f" | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/' || echo "n/a")
      active=$(grep -oE '"active"[[:space:]]*:[[:space:]]*(true|false)' "$f" | head -1 | grep -oE '(true|false)$' || echo "?")
    fi
    [ -z "$iter" ] && iter="n/a"
    [ -z "$maxiter" ] && maxiter="n/a"
    [ -z "$started" ] && started="n/a"
    [ -z "$active" ] && active="?"
    echo "  - $mode — active=$active, iteration=$iter/$maxiter, started=$started"
  done
fi

# Delete state files
DELETED=0
for f in "${FOUND[@]}"; do
  if rm -f "$f"; then
    DELETED=$((DELETED + 1))
  fi
done

# Prune empty session dirs (always, not just on --force — keeps state clean)
PRUNED=0
if [ -d "$STATE_DIR/sessions" ]; then
  while IFS= read -r -d '' dir; do
    rmdir "$dir" 2>/dev/null && PRUNED=$((PRUNED + 1))
  done < <(find "$STATE_DIR/sessions" -mindepth 1 -maxdepth 1 -type d -empty -print0 2>/dev/null)
fi

echo ""
echo "Cancelled $DELETED state file(s). Pruned $PRUNED empty session dir(s)."
echo "Loop will terminate on next Stop hook evaluation."

After running the block, summarize in plain text: how many files were cleaned, which modes were active, and confirm the loop will exit. If $ARGUMENTS contained --force and no files were found, explicitly say so.

Design notes

  • Path-safe: uses bash arrays and find -print0 to handle paths with spaces (e.g. C:\Users\roger\My Projects\...).
  • jq-optional: falls back to grep/sed when jq isn't installed (Windows Git Bash commonly lacks jq).
  • Scope-limited: only removes files under .dog/state/. Never touches git, source code, .dog/PRPs/, or anything else.
  • Idempotent: running twice is a no-op.
  • Empty-dir pruning: always cleans empty session directories to keep .dog/state/sessions/ tidy, even without --force.
  • Modeled after: anthropics/claude-plugins-official/plugins/ralph-loop/commands/cancel-ralph.md (single-file cancel pattern) + adapted for dog_stack's session-scoped multi-mode state layout.

When to use

  • After a persistence loop completes and you want to stop the Stop hook from re-injecting prompts
  • When a loop is stuck awaiting confirmation and you want to exit manually
  • When state files are corrupt or point to a dead session (--force)
  • Before starting a new persistence loop to avoid collisions with stale state