ff-run.sh 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env bash
  2. # ff-run.sh - whole-run replay/status for a fleetflow run.
  3. #
  4. # resume: replay every packet in <run>/manifest.json through ff-spawn, IN
  5. # MANIFEST ORDER, sequential. Unchanged packets cache-hit (ff-spawn exit 3)
  6. # and are reported "cached"; changed/new packets run live. A per-lane summary
  7. # goes to stderr; a JSON result array goes to stdout. Exit 0 if every lane is
  8. # ok or cached, 10 if any lane failed.
  9. # status: convenience alias for ff-status (its JSON on stdout, identical exit).
  10. # stdout: the JSON result list (resume) / ff-status JSON (status). stderr: chatter.
  11. #
  12. # Exit codes: 0 all ok/cached | 2 usage | 10 a lane failed
  13. set -u
  14. . "$(dirname "${BASH_SOURCE[0]}")/_env.sh"
  15. FF_VERSION="1.1.0"
  16. usage() {
  17. cat <<'EOF'
  18. Usage: ff-run.sh resume --run NAME [--repo PATH]
  19. ff-run.sh status --run NAME [--repo PATH]
  20. resume --run NAME replay every packet in <run>/manifest.json through
  21. ff-spawn, in order. Unchanged packets cache-hit ("cached"),
  22. changed/new ones run live. JSON result list on stdout.
  23. status --run NAME alias for ff-status (run status JSON on stdout).
  24. --repo PATH repo root (default: git toplevel of cwd)
  25. EXAMPLES
  26. ff-run.sh resume --run currency
  27. ff-run.sh resume --run currency --repo /path/to/repo | jq '.[] | select(.status!="cached")'
  28. ff-run.sh status --run currency | jq '.lanes | length'
  29. EOF
  30. }
  31. err() { echo "ff-run: $*" >&2; }
  32. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  33. SPAWN="$HERE/ff-spawn.sh"
  34. MODE="" RUN="" REPO=""
  35. [ $# -gt 0 ] || { err "a subcommand is required (resume|status)"; usage >&2; exit 2; }
  36. case "$1" in
  37. resume) MODE="resume"; shift ;;
  38. status) MODE="status"; shift ;;
  39. -h|--help) usage; exit 0 ;;
  40. *) err "unknown subcommand: $1"; usage >&2; exit 2 ;;
  41. esac
  42. while [ $# -gt 0 ]; do
  43. case "$1" in
  44. --run) RUN="${2:-}"; shift 2 ;;
  45. --repo) REPO="${2:-}"; shift 2 ;;
  46. -h|--help) usage; exit 0 ;;
  47. *) err "unknown argument: $1"; usage >&2; exit 2 ;;
  48. esac
  49. done
  50. command -v jq >/dev/null || { err "jq required"; exit 2; }
  51. command -v git >/dev/null || { err "git required"; exit 2; }
  52. [ -n "$RUN" ] || { err "--run required"; usage >&2; exit 2; }
  53. [ -n "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)" || true
  54. [ -n "$REPO" ] && [ -d "$REPO" ] || { err "not in a git repo (or --repo invalid)"; exit 2; }
  55. # status = alias for ff-status (hand off and let it own exit codes).
  56. # exec THROUGH bash: a direct exec needs the +x bit, which zip installs and
  57. # mode-dropping checkouts lose — Git Bash fakes modes, so only Linux notices.
  58. if [ "$MODE" = "status" ]; then
  59. if [ -n "$REPO" ]; then exec bash "$HERE/ff-status.sh" --run "$RUN" --repo "$REPO"; fi
  60. exec bash "$HERE/ff-status.sh" --run "$RUN"
  61. fi
  62. RUNDIR="$REPO/.fleetflow/$RUN"
  63. MANIFEST="$RUNDIR/manifest.json"
  64. [ -f "$MANIFEST" ] || { err "no manifest at $MANIFEST (run ff-spawn first)"; exit 2; }
  65. # Snapshot the packets ONCE, before replay. ff-spawn upserts each packet on the
  66. # way in (remove-then-append), which REORDERS the live manifest - so re-reading
  67. # .packets[$i] mid-loop would drift and revisit the same packet. The snapshot is
  68. # the replay contract: spawn order = the order captured here, frozen.
  69. PACKETS="$(jq -c '.packets' "$MANIFEST" 2>/dev/null)"
  70. N="$(printf '%s' "$PACKETS" | jq -r 'length' 2>/dev/null)"
  71. [ "${N:-0}" -gt 0 ] 2>/dev/null || { err "manifest has no packets to replay"; exit 2; }
  72. err "resume: replaying $N packet(s) from $MANIFEST (sequential)"
  73. err " # id brain status"
  74. err " -- ----------------------- -------- --------"
  75. RESULTS="[]"
  76. ANY_FAIL=0
  77. i=0
  78. while [ "$i" -lt "$N" ]; do
  79. pid="$(printf '%s' "$PACKETS" | jq -r ".[$i].id")"
  80. pbrain="$(printf '%s' "$PACKETS" | jq -r ".[$i].brain")"
  81. # imported native packets are terminal facts, not replayable ("native" is not a
  82. # spawnable brain - ff-spawn rejects it). ff-run resume SKIPS them; to continue
  83. # from an imported result, spawn a fresh lane with a real brain. See ff-import.
  84. if [ "$pbrain" = "native" ]; then
  85. err " $((i+1)) $(printf '%-23s' "$pid") native imported (skipped)"
  86. RESULTS="$(jq -nc --argjson R "$RESULTS" --arg id "$pid" --arg s "imported" --argjson rc 0 \
  87. '$R + [{id:$id,status:$s,rc:$rc}]')"
  88. i=$((i+1)); continue
  89. fi
  90. pphase="$(printf '%s' "$PACKETS" | jq -r ".[$i].phase // \"build\"")"
  91. ppf="$(printf '%s' "$PACKETS" | jq -r ".[$i].prompt_file")"
  92. pwt="$(printf '%s' "$PACKETS" | jq -r ".[$i].worktree // false")"
  93. pmt="$(printf '%s' "$PACKETS" | jq -r ".[$i].max_turns // 100")"
  94. peff="$(printf '%s' "$PACKETS" | jq -r ".[$i].effort // \"\"")"
  95. psch="$(printf '%s' "$PACKETS" | jq -r ".[$i].schema // \"\"")"
  96. # worktree is a boolean string ("true"/"false"); both are non-empty, so gate on
  97. # the literal value rather than ${pwt:+...} (which would always fire).
  98. WT_FLAG=""; [ "$pwt" = "true" ] && WT_FLAG="1"
  99. bash "$SPAWN" --run "$RUN" --id "$pid" --brain "$pbrain" --phase "$pphase" \
  100. --prompt-file "$ppf" --max-turns "$pmt" --repo "$REPO" \
  101. ${WT_FLAG:+--worktree} ${peff:+--effort "$peff"} ${psch:+--schema "$psch"} \
  102. >/dev/null 2>>"$RUNDIR/$pid.resume.err"
  103. rc=$?
  104. case "$rc" in
  105. 0) status="ran" ;;
  106. 3) status="cached" ;;
  107. *) status="failed"; ANY_FAIL=1 ;;
  108. esac
  109. err " $((i+1)) $(printf '%-23s' "$pid") $(printf '%-8s' "$pbrain") $status${rc:+ (rc=$rc)}"
  110. RESULTS="$(jq -nc --argjson R "$RESULTS" --arg id "$pid" --arg s "$status" --argjson rc "$rc" \
  111. '$R + [{id:$id,status:$s,rc:$rc}]')"
  112. i=$((i+1))
  113. done
  114. RAN="$(printf '%s' "$RESULTS" | jq -r '[.[]|select(.status=="ran")]|length')"
  115. CACHED="$(printf '%s' "$RESULTS" | jq -r '[.[]|select(.status=="cached")]|length')"
  116. FAILED="$(printf '%s' "$RESULTS" | jq -r '[.[]|select(.status=="failed")]|length')"
  117. err " --"
  118. err " summary: $RAN ran, $CACHED cached, $FAILED failed"
  119. printf '%s\n' "$RESULTS"
  120. [ "$ANY_FAIL" = 1 ] && exit 10
  121. exit 0