#!/usr/bin/env bash
# Copyright 2026 Mohsen Seyedkazemi Ardebili
# SPDX-License-Identifier: Apache-2.0
# Part of agent-fleet — https://github.com/MSKazemi/agent-fleet
# fleet — run parallel Claude agents against a per-project task queue.
#
# One command, two halves: queue subcommands are handled by libexec/fleet-queue,
# window subcommands (which drive VS Code through xdotool) are handled here.
#
# The project is resolved from where you ARE, not from where this script lives:
# $AGENT_ROOT, else the nearest ancestor holding .agents/, else the git root,
# else $PWD. One installed copy serves every repo on the machine.
set -euo pipefail

SELF="$(readlink -f "${BASH_SOURCE[0]}")"
REPO="$(cd "$(dirname "${SELF}")/.." && pwd)"
QUEUE_BIN="${REPO}/libexec/fleet-queue"
DEFAULT_DELAY=10

die() { echo "fleet: $*" >&2; exit 1; }

[[ -x "${QUEUE_BIN}" ]] || die "queue engine missing at ${QUEUE_BIN} (re-run bin/install)"

resolve_root() {
  if [[ -n "${AGENT_ROOT:-}" ]]; then echo "${AGENT_ROOT}"; return; fi
  local dir="${PWD}"
  while [[ "${dir}" != "/" ]]; do
    [[ -d "${dir}/${AGENT_DIR:-.agents}" ]] && { echo "${dir}"; return; }
    dir="$(dirname "${dir}")"
  done
  local top
  top="$(git rev-parse --show-toplevel 2>/dev/null || true)"
  echo "${top:-${PWD}}"
}

ROOT="$(resolve_root)"

# THREE PATHS, NOT ONE — they coincide in the usual case and must stay separate in the rest.
#
#   ROOT        whose queue is in scope.
#   AGENTS      where that queue, its logs, config and probe live: <ROOT>/$AGENT_DIR, which is
#               ".agents" unless the project names it otherwise (same variable the queue engine
#               reads, so the two halves cannot disagree about where the queue is).
#   WORKSPACE   the folder VS Code has OPEN. tasks.json is only ever read from there and the
#               window title is named after it. Defaults to ROOT; a project that keeps its queue
#               in a subfolder of the repo it opens sets FLEET_WORKSPACE to that repo, or
#               tasks.json is written somewhere VS Code never looks.
AGENTS="${ROOT}/${AGENT_DIR:-.agents}"
WORKSPACE="${FLEET_WORKSPACE:-${ROOT}}"
PROJECT="$(basename "${WORKSPACE}")"
TASKS="${WORKSPACE}/.vscode/tasks.json"

# What a worker needs in its environment to find THIS queue. AGENT_DIR is forwarded only when
# set: a VS Code task does not inherit this shell, so a renamed queue would otherwise be
# invisible to the very workers it was built for.
WORKER_ENV="AGENT_ROOT=${ROOT}${AGENT_DIR:+ AGENT_DIR=${AGENT_DIR}}"

queue() { AGENT_ROOT="${ROOT}" "${QUEUE_BIN}" "$@"; }

# --------------------------------------------------------------------------- #
# help
# --------------------------------------------------------------------------- #

usage() {
  cat <<'EOF'
fleet — parallel Claude agents against a per-project task queue

SETUP
  fleet init [--max-workers N]   set this project up (queue + git exclude rule)
  fleet where                    which project's queue is in scope
  fleet machine                  what this computer can spare, and why
  fleet doctor [--fix]           prove nothing can leak, and that work is runnable
  fleet --version                print the installed version
  fleet probe-browser            ask a worker whether it can actually open a tab

QUEUE WORK
  fleet add URL|TEXT [--title T] [--note N] [--priority 1-9] [--kind K]
                     [--exec browser|agent|serial] [--serial]
  fleet add-batch [FILE|-]       one per line: 'URL | title | note'
  fleet from-note [--section S]  promote checklist items (TASKS.md) into the queue
  fleet to-note                  tick finished items in the note, log them

RUN IT
  fleet plan [--max N]           the schedule: lanes, terminals, and why
  fleet run [N] [--max N] [--delay S] [--headless|--terminals] [--dry]
                                 start the browser lane; terminals when X11 is
                                 available, detached processes otherwise
  fleet open N                   N empty terminals, nothing started
  fleet launch N --cmd C [--headless]   N workers all running C

WATCH IT
  fleet status                   project, agents, queue counts
  fleet list [STATE]             tasks by state
  fleet watch                    live counters
  fleet blocked                  what needs you, grouped by reason
  fleet unblock ID | --all       release blocked task(s) back to the queue
  fleet reset [--older-than S]   recover tasks from an agent that died
  fleet clean                    remove the generated .vscode/tasks.json

FOR AGENTS
  fleet claim AGENT [--exec LANE]        take the next task (exit 3 = lane empty)
  fleet done  ID --result "..."          finish, with the confirmation you saw
  fleet block ID --reason "..."          needs a human; notifies and frees you
  fleet fail  ID --reason "..."          give up
  fleet show  ID                         one task as JSON

Any subcommand takes --help for its own options.
Docs: the agent-fleet skill, or README.md in this tool's repo.
EOF
}

# --------------------------------------------------------------------------- #
# window / launcher half
# --------------------------------------------------------------------------- #

gui_available() {
  [[ "${XDG_SESSION_TYPE:-}" == "x11" ]] && command -v xdotool >/dev/null
}

gui_missing_reason() {
  [[ "${XDG_SESSION_TYPE:-}" == "x11" ]] \
    || { echo "not an X11 session (XDG_SESSION_TYPE=${XDG_SESSION_TYPE:-unset})"; return; }
  echo "xdotool is not installed (apt install xdotool)"
}

require_gui() {
  gui_available || die "$(gui_missing_reason); use --headless to run without terminals"
}

# Standalone terminal windows: the visible mode that needs neither X11 nor xdotool.
#
# The VS Code path presses a key into another application's window, which only works on an X11
# session. Most current desktops are Wayland, where `fleet run` used to fall back to headless —
# a print-mode session with NO Chrome tools, so every browser task came back blocked. A terminal
# emulator needs none of that: it is started like any program, opens its own window, and the
# `claude` inside it is a normal interactive session, which is the kind that is offered Chrome.
EMULATORS=(gnome-terminal kitty alacritty konsole xfce4-terminal x-terminal-emulator xterm)

find_emulator() {
  local t
  [[ -n "${FLEET_TERMINAL:-}" ]] && { command -v "${FLEET_TERMINAL}" && return; }
  for t in "${EMULATORS[@]}"; do command -v "${t}" && return; done
  return 1
}

windows_available() {
  [[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]] && find_emulator >/dev/null
}

# emulator_argv TITLE CMD  — print the argv (NUL-separated) that opens CMD in a new window.
emulator_argv() {
  local title="$1" cmd="$2" bin name
  bin="$(find_emulator)" || return 1
  name="$(basename "${bin}")"
  case "${name}" in
    gnome-terminal)    printf '%s\0' "${bin}" --title "${title}" -- bash -lc "${cmd}" ;;
    kitty)             printf '%s\0' "${bin}" --title "${title}" bash -lc "${cmd}" ;;
    alacritty)         printf '%s\0' "${bin}" --title "${title}" -e bash -lc "${cmd}" ;;
    xfce4-terminal)    printf '%s\0' "${bin}" --title "${title}" -x bash -lc "${cmd}" ;;
    konsole)           printf '%s\0' "${bin}" -e bash -lc "${cmd}" ;;
    *)                 printf '%s\0' "${bin}" -T "${title}" -e bash -lc "${cmd}" ;;
  esac
}

# launch_windows CWD N CMD...  — one window per worker, staggered, admission-controlled.
launch_windows() {
  local cwd="$1" n="$2"; shift 2
  local cmds=("$@") i started=0 inner argv=()
  mkdir -p "${AGENTS}/logs"
  for i in $(seq 1 "${n}"); do
    if [[ "${i}" -gt 1 ]]; then
      if [[ "${FORCE:-0}" -eq 0 ]]; then
        local room
        room="$(queue machine --json | python3 -c 'import json,sys;print(json.load(sys.stdin)["capacity"])')"
        if [[ "${room}" -lt 1 ]]; then
          echo "  stopping at $((i - 1)) worker(s): no headroom left on this machine"
          echo "  the remaining tasks stay queued — run fleet run again when it frees up"
          break
        fi
      fi
      sleep "${STAGGER:-8}"
    fi
    # `exec` would replace the shell and close the window the instant a worker dies, taking its
    # error with it. Keep the window until someone has read why.
    inner="cd '${cwd}' && ${cmds[$((i-1))]/ exec / }; rc=\$?; echo; echo \"[agent-${i}] exited with status \${rc} - press Enter to close\"; read -r _"
    mapfile -d '' argv < <(emulator_argv "agent-${i}" "${inner}")
    if setsid "${argv[@]}" >>"${AGENTS}/logs/agent-${i}.launch.log" 2>&1 </dev/null & then
      started=$((started + 1))
      echo "  agent-${i}  window opened (${argv[0]##*/})"
    fi
  done
  echo "opened ${started}/${n} window(s); each is one interactive Claude session started with --chrome"
  [[ "${started}" -ge 1 ]]
}

# Start workers as detached background processes instead of VS Code terminals.
# No X11, no window, no focus stolen — the trade is that you read progress from
# .agents/logs/agent-N.log instead of watching a terminal panel.
launch_headless() {
  local cwd="$1" n="$2"; shift 2
  local cmds=("$@") i pid logfile started=0 alive=0 pids=()
  mkdir -p "${AGENTS}/logs"
  for i in $(seq 1 "${n}"); do
    # Admission control: the machine is re-read between starts, because the
    # workers already running are themselves changing the answer.
    if [[ "${i}" -gt 1 && "${FORCE:-0}" -eq 0 ]]; then
      local room
      room="$(queue machine --json | python3 -c 'import json,sys;print(json.load(sys.stdin)["capacity"])')"
      if [[ "${room}" -lt 1 ]]; then
        echo "  stopping at $((i - 1)) worker(s): no headroom left on this machine"
        echo "  the remaining tasks stay queued — run fleet run again when it frees up"
        break
      fi
    fi
    logfile="${AGENTS}/logs/agent-${i}.log"
    setsid bash -c "cd '${cwd}' && ${cmds[$((i-1))]}" >>"${logfile}" 2>&1 &
    pid=$!
    pids+=("${pid}")
    kill -0 "${pid}" 2>/dev/null && started=$((started + 1))
    echo "  agent-${i}  pid ${pid}  → ${logfile}"
  done
  sleep 5
  for pid in "${pids[@]}"; do kill -0 "${pid}" 2>/dev/null && alive=$((alive + 1)); done
  echo "started ${started}/${n}; still running after 5s: ${alive}"
  if [[ "${started}" -lt "${n}" ]]; then
    echo "WARNING: not every worker started — read ${AGENTS}/logs/agent-*.log" >&2
    return 1
  fi
  [[ "${alive}" -lt "${n}" ]] && echo "note: $(( n - alive )) already exited — that is normal for a short queue, but check the logs if unexpected"
  echo "follow them with:  tail -f ${AGENTS}/logs/agent-*.log"
  return 0
}

# Prefer the window carrying this project; else, if exactly one VS Code window is
# open, use it; else refuse rather than drive the wrong project's window.
vscode_window() {
  local win all
  win="$(xdotool search --name " - ${PROJECT} - Visual Studio Code" 2>/dev/null | head -1)"
  [[ -n "${win}" ]] && { echo "${win}"; return; }
  all="$(xdotool search --name "Visual Studio Code$" 2>/dev/null || true)"
  if [[ "$(wc -l <<<"${all}")" -eq 1 && -n "${all}" ]]; then echo "${all}"; return; fi
  die "no VS Code window for '${PROJECT}' — open ${ROOT} in VS Code first"
}

count_shells() { pgrep -c -u "$(id -u)" bash || true; }

# Count only the agents working THIS project. Every worker is started with
# AGENT_ROOT in its environment, which survives `exec` — unlike the command line,
# and unlike the cwd, which may be another repo when workers_start_in is set.
# A machine-wide
# pgrep would count every unrelated Claude session and make the check meaningless.
count_agents() {
  local pid count=0
  for pid in $(pgrep -f -u "$(id -u)" 'claude( --chrome)? --dangerously-skip-permissions' 2>/dev/null || true); do
    if grep -qz "AGENT_ROOT=${ROOT}" "/proc/${pid}/environ" 2>/dev/null \
       || [[ "$(readlink -f "/proc/${pid}/cwd" 2>/dev/null)" == "${ROOT}" ]]; then
      count=$((count + 1))
    fi
  done
  echo "${count}"
}

send_key() {
  local win="$1" key="$2"
  xdotool windowactivate --sync "${win}" 2>/dev/null
  xdotool key --clearmodifiers "${key}"
}

# write_tasks DELAY CMD...  — one task per CMD plus a compound default build task.
# JSON is emitted by python3 so commands containing quotes cannot corrupt it, and
# any tasks the project already had are preserved.
write_tasks() {
  local delay="$1"; shift
  mkdir -p "${WORKSPACE}/.vscode" "${AGENTS}"
  python3 - "${TASKS}" "${delay}" "${AGENTS}/tasks.json.orig" "${STAGGER:-8}" "$@" <<'PY'
import json, os, re, shutil, sys
out, delay, orig, stagger, cmds = (sys.argv[1], sys.argv[2], sys.argv[3],
                                   sys.argv[4], sys.argv[5:])

OURS = re.compile(r"agent-\d+$")


def load(path):
    """tasks.json is JSONC in practice — tolerate // comments and trailing commas."""
    raw = open(path).read()
    for candidate in (raw,
                      re.sub(r",(\s*[}\]])", r"\1",
                             re.sub(r"^\s*//.*$", "", raw, flags=re.M))):
        try:
            return json.loads(candidate)
        except json.JSONDecodeError:
            continue
    return None


document, kept = {}, []
if os.path.exists(out):
    data = load(out)
    if data is None:
        shutil.copy(out, out + ".backup")
        print(f"warning: could not parse {out}; backed it up to {out}.backup",
              file=sys.stderr)
    else:
        document = data
        kept = [t for t in data.get("tasks", [])
                if not OURS.match(t.get("label", "")) and t.get("label") != "agents: all"]
        if kept and not os.path.exists(orig):
            shutil.copy(out, orig)     # pristine copy, restored by `fleet clean`
            print(f"kept {len(kept)} existing task(s); original saved to {orig}",
                  file=sys.stderr)

tasks = []
for i, cmd in enumerate(cmds, start=1):
    label = f"agent-{i}"
    # Stagger the starts: a worker's memory arrives over a few seconds, so
    # landing them together is what spikes a machine, not the count itself.
    wait = int(delay) + (i - 1) * int(stagger)
    prefix = f"echo '[{label}] starting in {wait}s'; sleep {wait}; " if wait else ""
    tasks.append({
        "label": label,
        "type": "shell",
        "command": prefix + cmd,
        "isBackground": True,
        "presentation": {"panel": "dedicated", "group": "agents", "focus": False},
        "problemMatcher": [],
    })
tasks.append({
    "label": "agents: all",
    "dependsOn": [t["label"] for t in tasks],
    "dependsOrder": "parallel",
    "group": {"kind": "build", "isDefault": True},
    "problemMatcher": [],
})

# Our compound task must be the only default build task, or Ctrl+Shift+B asks.
for task in kept:
    group = task.get("group")
    if isinstance(group, dict) and group.get("isDefault"):
        task["group"] = {**group, "isDefault": False}

document.setdefault("version", "2.0.0")
document["tasks"] = kept + tasks
with open(out, "w") as fh:
    json.dump(document, fh, indent=2)
    fh.write("\n")
PY
}

trigger_tasks() {
  local n="$1" delay="$2" win before after
  require_gui
  win="$(vscode_window)"
  before="$(count_agents)"
  send_key "${win}" ctrl+shift+b
  echo "started the build task; waiting $((delay + 12))s for the agents to come up..."
  sleep "$((delay + 12))"
  after="$(count_agents)"
  echo "agents in ${PROJECT}: ${before} -> ${after}"
  [[ "${after}" -ge "$((before + n))" ]] || cat >&2 <<EOF
WARNING: expected ${n} new agent(s) but see $((after - before)).
  If VS Code showed a task picker, run 'Tasks: Run Build Task' once by hand.
  If a worker exited immediately, check the terminal panel for its error.
EOF
}

cmd_open() {
  local n="${1:-1}" win before after
  require_gui
  win="$(vscode_window)"
  before="$(count_shells)"
  for _ in $(seq 1 "${n}"); do
    send_key "${win}" ctrl+shift+grave
    sleep 1
  done
  sleep 1
  after="$(count_shells)"
  echo "opened ${n} terminal(s) in window ${win}; shells: ${before} -> ${after}"
  [[ "${after}" -gt "${before}" ]] || die "shell count did not grow — no terminal appeared"
}

cmd_launch() {
  local n=1 delay="${DEFAULT_DELAY}" headless=0 cmd="claude --dangerously-skip-permissions" cmds=()
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --delay)    delay="${2:?--delay needs seconds}"; shift 2 ;;
      --cmd)      cmd="${2:?--cmd needs a command}"; shift 2 ;;
      --headless) headless=1; shift ;;
      [0-9]*)     n="$1"; shift ;;
      -h|--help)  echo "usage: fleet launch N --cmd C [--delay S] [--headless]"; return 0 ;;
      *)          die "unknown argument: $1" ;;
    esac
  done
  for _ in $(seq 1 "${n}"); do cmds+=("${cmd}"); done
  if [[ "${headless}" -eq 1 ]]; then
    echo "starting ${n} headless process(es):"
    launch_headless "${ROOT}" "${n}" "${cmds[@]}"
    return
  fi
  write_tasks "${delay}" "${cmds[@]}"
  echo "wrote ${TASKS} (${n} task(s), delay ${delay}s, command: ${cmd})"
  trigger_tasks "${n}" "${delay}"
}

# --------------------------------------------------------------------------- #
# is there a browser? — ask a worker, do not assume
# --------------------------------------------------------------------------- #

# The tool a worker needs before it can obey the "open your own tab" rule.
BROWSER_TOOL="mcp__claude-in-chrome__tabs_create_mcp"
PROBE_FILE="${AGENTS}/browser-probe.json"

worker_command() {
  python3 - "${AGENTS}/config.json" <<'PY'
import json, sys
try:
    print(json.load(open(sys.argv[1]))["worker_command"])
except Exception:
    print("claude --dangerously-skip-permissions")
PY
}

# A browser lane is only as real as a worker's ability to open a tab, and that
# access is NOT inherited from the session you type in — it belongs to whichever
# session the Chrome extension is attached to. Same principle as `fleet machine`:
# measure it, on this machine, now, instead of assuming it.
cmd_probe_browser() {
  local timeout=180 dry=0 cwd cmd out first verdict question
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --timeout) timeout="${2:?--timeout needs seconds}"; shift 2 ;;
      --dry)     dry=1; shift ;;
      -h|--help) cat <<'EOF'
usage: fleet probe-browser [--timeout S] [--dry]

Starts one short-lived session exactly the way `fleet run` starts a worker, and
asks it whether the Chrome tab tool is there. The answer is recorded in
.agents/browser-probe.json, and `fleet doctor` and `fleet run` read it.

  --timeout S   give up after S seconds (default 180)
  --dry         print what would be asked, ask nothing, record nothing

exit 0 = a worker can open a tab   1 = it cannot   2 = the probe itself failed
EOF
                 return 0 ;;
      *)         die "unknown argument: $1" ;;
    esac
  done

  [[ -d "${AGENTS}" ]] || die "no fleet project here — run: fleet init"
  cmd="$(worker_command)"
  cwd="$(python3 -c 'import json,sys;print(json.loads(sys.argv[1])["worker_cwd"])' \
         "$(queue plan --json)")"
  question="Answer on the first line with one word, YES or NO: is a tool named ${BROWSER_TOOL} available to you right now? Do not call any tool and do not open a browser. After the first line, name any browser tools you can see."

  if [[ "${dry}" -eq 1 ]]; then
    echo "--dry: would run in ${cwd}"
    echo "  ${cmd} -p '<one YES/NO question about ${BROWSER_TOOL}>'"
    echo "  and record the answer in ${PROBE_FILE}"
    return 0
  fi

  echo "asking one worker-shaped session whether it has a browser (up to ${timeout}s)..."
  out="$(cd "${cwd}" && env ${WORKER_ENV} AGENT_ID=probe \
         timeout "${timeout}" setsid ${cmd} -p "${question}" </dev/null 2>&1 || true)"
  first="$(grep -oE '[A-Za-z]+' <<<"${out}" | head -1 | tr '[:lower:]' '[:upper:]')"

  case "${first}" in
    YES) verdict=true  ;;
    NO)  verdict=false ;;
    *)   echo "probe failed: no YES/NO came back in ${timeout}s — nothing recorded" >&2
         [[ -n "${out}" ]] && printf '  it said: %s\n' "$(head -3 <<<"${out}")" >&2
         return 2 ;;
  esac

  python3 - "${PROBE_FILE}" "${verdict}" "${cmd}" "${out}" <<'PY'
import json, sys, time
path, verdict, cmd, out = sys.argv[1:5]
json.dump({
    "checked_at": time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime()),
    "browser": verdict == "true",
    "tool": "mcp__claude-in-chrome__tabs_create_mcp",
    "worker_command": cmd,
    # The probe asks with -p, so it measures PRINT mode. A terminal worker is
    # the same binary run interactively, and that is a different answer: an
    # interactive session is offered the Chrome toolset and gets its own
    # per-session tab group, measured on 2026-08-16 with two sessions holding
    # live connections to one Chrome at once. Recording the mode keeps a NO
    # here from being read as a verdict on `fleet run --terminals`.
    "mode": "print",
    "applies_to": "print/headless workers; terminal workers are not covered",
    "said": "\n".join(out.strip().splitlines()[:3]),
}, open(path, "w"), indent=2)
PY

  if [[ "${verdict}" == "true" ]]; then
    echo "YES — a worker session can open a Chrome tab; the browser lane is real here"
    echo "  recorded in ${PROBE_FILE}"
    return 0
  fi
  cat <<EOF
NO — a PRINT-mode session has no browser tools, so it cannot open a tab
  This probe asks with -p. It is the right answer for 'fleet run --headless',
  where browser-lane tasks will come back blocked rather than done.
  It is NOT a verdict on 'fleet run --terminals': a terminal worker is the same
  binary run interactively, it is offered the Chrome toolset, and it gets its
  own per-session tab group. Two sessions can hold live connections to one
  Chrome at the same time — measured, not assumed.
  So: use terminals for browser work, or move it to a session that has a tab.
  recorded in ${PROBE_FILE}
EOF
  return 1
}

# Say it before N workers start and block one task each.
#
# Scoped to the mode being started. The probe asks with -p, so a NO is evidence
# about print/headless workers only; terminal workers run the same binary
# interactively, are offered the Chrome toolset, and each get their own tab
# group. Warning on a terminal run would talk people out of the one arrangement
# that works.
warn_if_no_browser() {
  local headless="${1:-1}"
  python3 - "${PROBE_FILE}" "${headless}" <<'PY'
import json, sys
try:
    p = json.load(open(sys.argv[1]))
except Exception:
    sys.exit(0)
headless = sys.argv[2] == "1"
if p.get("browser", True):
    sys.exit(0)
when = str(p.get("checked_at", "?"))[:10]
if p.get("mode", "print") != "print":
    print(f"WARNING: the last browser probe ({when}) found no browser in a worker.")
    print("  These workers will block their tasks rather than do them.")
elif headless:
    print(f"WARNING: the last browser probe ({when}) found no browser in a print-mode")
    print("  session, and this is a headless run — the closest thing to it. Browser")
    print("  tasks will likely come back blocked. Prefer: fleet run --terminals")
else:
    print(f"note: the browser probe ({when}) said no, but it measures -p (print mode).")
    print("  This is a terminal run, where each worker gets its own Chrome tab group.")
PY
}

# The worker prompt is embedded in a single-quoted shell string inside a JSON
# string, so it deliberately contains no single quotes.
worker_prompt() {
  local id="$1" ask=""
  [[ "${ASK:-0}" -eq 1 ]] && ask=" This run is interactive: at a login, captcha, 2FA or a question only the human can answer, still run fleet block with the reason and leave your tab open, then stay in this window and tell the human exactly what you need. When they reply, run fleet unblock with the task id (add --answer with their words if they answered a question), claim again and continue in the same tab. The other workers keep going while you wait. Never enter credentials or solve a captcha yourself."
  echo "You are ${id}, one browser worker in a parallel fleet. Load the agent-fleet skill and follow its worker protocol reference exactly: claim a task with fleet claim ${id} --exec browser, do it in your own Chrome tab, then report it with fleet done/block/fail. Repeat until fleet claim reports the lane is empty, then stop and print a summary of what you completed.${ask}"
}

cmd_run() {
  local n="" delay="${DEFAULT_DELAY}" dry=0 force=0 max="" headless="" windows=0 vscode=0 i cmds=() ASK=0 chrome=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --delay)    delay="${2:?--delay needs seconds}"; shift 2 ;;
      --max)      max="${2:?--max needs a number}"; shift 2 ;;
      --dry)      dry=1; shift ;;
      --force)    force=1; shift ;;
      --stagger)  STAGGER="${2:?--stagger needs seconds}"; export STAGGER; shift 2 ;;
      --headless) headless=1; shift ;;
      --terminals) headless=0; shift ;;
      --windows)  headless=0; windows=1; shift ;;
      --vscode)   headless=0; vscode=1; shift ;;
      --ask)      ASK=1; shift ;;
      auto)       n=""; shift ;;
      [0-9]*)     n="$1"; shift ;;
      -h|--help)  cat <<'EOF'
usage: fleet run [N] [--max N] [--delay S] [--headless|--terminals|--windows|--vscode] [--ask] [--dry]

  N            how many browser workers (default: whatever `fleet plan` says)
  --max N      cap the worker count for this run
  --delay S    seconds before each worker starts (default 10; terminals only)
  --headless   run workers as background processes, logging to .agents/logs/
               (no window, no focus stolen) — but a headless session has no Chrome, so
               browser tasks come back blocked. Chosen only when there is no display.
  --force      ignore what the machine can spare (can overload it)
  --stagger S  seconds between worker starts (default 8) so memory ramps
  --terminals  VS Code terminals (X11 + xdotool); with no X11 it opens windows instead
  --ask        interactive workers: at a login, captcha or question they stay in their
               own window and tell you what they need, then resume when you answer there.
               Default is to park the task and move on (see the worker protocol).
  --vscode     VS Code terminals only, never windows (needs X11 + xdotool to start them)
  --windows    one standalone terminal-emulator window per worker — works on Wayland,
               needs no xdotool. $FLEET_TERMINAL picks the emulator (default: first of
               gnome-terminal, kitty, alacritty, konsole, xfce4-terminal, xterm found)
  --dry        write .vscode/tasks.json and stop
EOF
                  return 0 ;;
      *)          die "unknown argument: $1" ;;
    esac
  done

  [[ -d "${AGENTS}" ]] || die "no fleet project here — run: fleet init"

  # Size the fleet from the queue itself unless a count was given. Only the
  # browser lane needs terminals; agent and serial work runs in-session for free.
  if [[ -z "${n}" ]]; then
    echo "queue plan:"
    queue plan ${max:+--max "${max}"} $([[ "${force}" -eq 1 ]] && echo --force) | sed 's/^/  /'
    n="$(queue plan --count ${max:+--max "${max}"} $([[ "${force}" -eq 1 ]] && echo --force))"
    if [[ "${n}" -eq 0 ]]; then
      echo
      echo "no terminal needed for this queue."
      python3 - "$(queue plan --json)" <<'PY'
import json, sys
plan = json.loads(sys.argv[1])
if plan["pending"] == 0:
    print("  nothing queued — add work with: fleet add ...")
else:
    print("  run the remaining lanes here, without terminals:")
    if plan["lanes"]["agent"]:
        print(f"    {plan['lanes']['agent']} agent task(s):  fleet claim orchestrator "
              f"--exec agent   ({plan['subagents']} in parallel)")
    if plan["lanes"]["serial"]:
        print(f"    {plan['lanes']['serial']} serial task(s): fleet claim orchestrator "
              f"--exec serial  (one at a time)")
PY
      return 0
    fi
  fi

  # An explicit count is a request, not a licence to overload the machine.
  if [[ "${force}" -eq 0 ]]; then
    local room
    room="$(python3 -c 'import json,sys;p=json.loads(sys.argv[1]);m=p["machine"];print(m["capacity"] if m else 10**6)' \
            "$(queue plan --json)")"
    if [[ "${n}" -gt "${room}" ]]; then
      echo "requested ${n} worker(s); this machine can spare ${room} right now — using ${room}."
      echo "  (override with --force, or free memory and try again)"
      n="${room}"
    fi
  fi

  # Skills load from the working directory, so workers_start_in can point a kind
  # of work at the repo holding its skills, while AGENT_ROOT keeps the queue here.
  local cwd
  cwd="$(python3 -c 'import json,sys;print(json.loads(sys.argv[1])["worker_cwd"])' \
         "$(queue plan --json)")"
  [[ "${cwd}" == "${ROOT}" ]] || echo "workers start in ${cwd} (workers_start_in), queue stays ${ROOT}"

  # VS Code terminals on X11; otherwise standalone windows if there is a display and an
  # emulator; headless only as the last resort (it cannot do browser work), or when forced.
  if [[ -z "${headless}" ]]; then
    if gui_available; then headless=0
    elif windows_available; then
      headless=0; windows=1
      echo "no usable X11 ($(gui_missing_reason)) — opening terminal windows instead"
    else
      headless=1
      echo "no usable X11 and no terminal emulator on a display — running headless"
    fi
  elif [[ "${headless}" -eq 0 && "${windows}" -eq 0 && "${vscode}" -eq 0 ]] \
       && ! gui_available && windows_available; then
    windows=1
    echo "no usable X11 ($(gui_missing_reason)) — opening terminal windows instead"
  fi
  [[ "${headless}" -eq 0 ]] && chrome=" --chrome"

  warn_if_no_browser "${headless}"

  for i in $(seq 1 "${n}"); do
    cmds+=("${WORKER_ENV} AGENT_ID=agent-${i} exec claude${chrome} --dangerously-skip-permissions '$(worker_prompt "agent-${i}")'")
  done

  if [[ "${headless}" -eq 1 ]]; then
    if [[ "${dry}" -eq 1 ]]; then
      echo "--dry: would start ${n} headless worker(s) in ${cwd}:"
      printf '  %s\n' "${cmds[@]}"
      return 0
    fi
    echo "starting ${n} headless worker(s):"
    FORCE="${force}" launch_headless "${cwd}" "${n}" "${cmds[@]}"
    return
  fi

  if [[ "${windows}" -eq 1 ]]; then
    if [[ "${dry}" -eq 1 ]]; then
      echo "--dry: would open ${n} terminal window(s) via $(basename "$(find_emulator)") in ${cwd}:"
      printf '  %s\n' "${cmds[@]}"
      return 0
    fi
    echo "opening ${n} terminal window(s):"
    FORCE="${force}" launch_windows "${cwd}" "${n}" "${cmds[@]}"
    return
  fi

  local prefixed=() c
  for c in "${cmds[@]}"; do prefixed+=("cd ${cwd} && ${c}"); done
  write_tasks "${delay}" "${prefixed[@]}"
  echo "wrote ${TASKS} (${n} browser worker(s), delay ${delay}s)"
  [[ "${dry}" -eq 1 ]] && { echo "--dry: tasks file written, nothing started"; return 0; }
  trigger_tasks "${n}" "${delay}"
}

cmd_status() {
  echo "project:  ${PROJECT}  (${ROOT})"
  echo "agents:   $(count_agents) running here"
  [[ -f "${TASKS}" ]] && echo "tasks:    ${TASKS}" || echo "tasks:    (none generated)"
  if [[ -d "${AGENTS}" ]]; then
    printf 'queue:    '
    queue status
  else
    echo "queue:    (not initialised — run 'fleet init')"
  fi
}

cmd_clean() {
  local orig="${AGENTS}/tasks.json.orig"
  if [[ -f "${orig}" ]]; then
    mv "${orig}" "${TASKS}"
    echo "restored the project's original ${TASKS}"
  else
    rm -f "${TASKS}"
    rmdir "${WORKSPACE}/.vscode" 2>/dev/null || true
    echo "removed ${TASKS}"
  fi
}

# --------------------------------------------------------------------------- #

case "${1:-}" in
  # window half
  run)    shift; cmd_run "$@" ;;
  probe-browser) shift; cmd_probe_browser "$@" ;;
  open)   shift; cmd_open "$@" ;;
  launch) shift; cmd_launch "$@" ;;
  status) shift; cmd_status "$@" ;;
  clean)  shift; cmd_clean "$@" ;;

  # queue half
  init|where|machine|doctor|add|add-batch|from-note|to-note|plan|list|watch|blocked|\
  unblock|reset|claim|done|block|fail|show)
          queue "$@" ;;

  version|--version|-V) echo "agent-fleet $(tr -d '[:space:]' < "${REPO}/VERSION")" ;;

  ""|help|-h|--help) usage ;;
  *) echo "fleet: unknown command '$1'" >&2; echo >&2; usage >&2; exit 2 ;;
esac
