#!/usr/bin/env bash
# {{PROJECT_NAME}} — agent worktree helper.
#
# Usage:
#   scripts/setup-agent-worktree.sh <name> [--task "<description>"] [--install] [--bead <id>]
#       Create or update .worktrees/<name>/ on branch agent/<name>.
#       Idempotent: re-running on an existing worktree is safe.
#       First runs an advisory duplicate-work scan (open/merged PRs + in-progress beads);
#       --task "<desc>" gives it better keywords than the bare <name>.
#       --install also runs the project's worktree setup commands (dependency installs, etc.)
#       inside the fresh worktree. Flags may appear in any order.
#       --bead <id> appends the bead id to the branch (agent/<name>/<id>) so open
#       branches show which beads are in flight; the worktree dir and the agent
#       identity stay per-agent. Refuses to rebind a live worktree that is
#       checked out on a different bead's branch.
#
#   scripts/setup-agent-worktree.sh --preflight-only --task "<description>"
#       Run only the duplicate-work scan (no worktree created).
#
#   scripts/setup-agent-worktree.sh --list
#       List all current worktrees with their branch and status.
#
# Conventions:
#   - Worktrees live under `.worktrees/<name>/` (gitignored).
#   - Each worktree's branch is named `agent/<name>` — or `agent/<name>/<bead-id>`
#     with --bead — and tracks origin/main initially.
#   - Each worktree gets a per-name git author identity for traceable blame.

set -euo pipefail

# Global variables for cleanup
CLEANUP_LOCK_DIR=""
PARENT_PID="${BASHPID:-$$}"

cleanup() {
  local current_pid="${BASHPID:-$$}"
  # Protect against subshell execution: BASH_SUBSHELL is supported in Bash 3.0+ and increments in every subshell.
  # This provides robust protection even on systems with Bash 3.2 where BASHPID is empty.
  if [[ "$current_pid" -eq "$PARENT_PID" ]] && [[ "${BASH_SUBSHELL:-0}" -eq 0 ]]; then
    if [[ -n "$CLEANUP_LOCK_DIR" && -d "$CLEANUP_LOCK_DIR" ]]; then
      # Only delete the lock directory if we still own it (verified by reading the pid file inside it)
      local holder_pid
      holder_pid="$(cat "$CLEANUP_LOCK_DIR/pid" 2>/dev/null || true)"
      if [[ "$holder_pid" -eq "$PARENT_PID" ]]; then
        rm -rf "$CLEANUP_LOCK_DIR" 2>/dev/null || true
      fi
    fi
  fi
}
trap cleanup EXIT

# ----- helpers -----

log()   { printf "→ %s\n" "$*"; }
# log() goes to stdout so test capture works; warn()/fatal() go to stderr.
warn()  { printf "! %s\n" "$*" >&2; }
fatal() { printf "✗ %s\n" "$*" >&2; exit 1; }

if [[ "$(git rev-parse --is-bare-repository 2>/dev/null || echo "false")" == "true" ]]; then
  fatal "bare repositories are not supported"
fi

GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
COMMON_DIR="$(git rev-parse --git-common-dir 2>/dev/null || true)"
if [[ -z "$GIT_DIR" || -z "$COMMON_DIR" ]]; then
  fatal "not inside a git repository"
fi

# Convert relative paths to absolute paths
if [[ "$GIT_DIR" != /* ]]; then
  GIT_DIR="$(pwd -P)/$GIT_DIR"
fi
GIT_DIR="$(cd "$GIT_DIR" >/dev/null 2>&1 && pwd -P || true)"

if [[ "$COMMON_DIR" != /* ]]; then
  COMMON_DIR="$(pwd -P)/$COMMON_DIR"
fi
COMMON_DIR="$(cd "$COMMON_DIR" >/dev/null 2>&1 && pwd -P || true)"

if [[ -z "$GIT_DIR" || -z "$COMMON_DIR" || ! -d "$COMMON_DIR" ]]; then
  fatal "failed to resolve absolute git directories"
fi

# If GIT_DIR is different from COMMON_DIR, we are executing inside a linked worktree.
# In that case, REPO_ROOT should point to the main repository checkout.
# We resolve the main worktree's path directly from the git worktree list.
if [[ "$GIT_DIR" != "$COMMON_DIR" ]]; then
  first_wt_line="$(git worktree list --porcelain 2>/dev/null | head -n 1 || true)"
  if [[ "$first_wt_line" =~ ^worktree[[:space:]](.+) ]]; then
    REPO_ROOT="${BASH_REMATCH[1]}"
  else
    REPO_ROOT=""
  fi
  if [[ -z "$REPO_ROOT" ]]; then
    REPO_ROOT="$(dirname "$COMMON_DIR")"
  fi
else
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
  if [[ -z "$REPO_ROOT" ]]; then
    fatal "Failed to resolve repository top-level directory"
  fi
fi
REPO_ROOT="$(cd "$REPO_ROOT" >/dev/null 2>&1 && pwd -P || true)"

if [[ -z "$REPO_ROOT" || ! -d "$REPO_ROOT" ]]; then
  fatal "resolved repository root '$REPO_ROOT' does not exist or is not a directory"
fi

WORKTREE_DIR="$REPO_ROOT/.worktrees"
mkdir -p "$WORKTREE_DIR"

usage() {
  sed -n '2,/^set -/p' "$0" | sed 's/^# \{0,1\}//; /^set -/d'
  exit 1
}

# Advisory duplicate-work scan: warn if an open or recently-merged PR (or in-progress bead)
# looks like the task about to be started. Never blocks; always returns 0.
# Matches task keywords against the raw text of PR titles (and in-progress beads), so it works
# whether `gh --jq` shaped the output or not — the scan is advisory, not a parser.
preflight_scan() {
  local desc="$1"
  [[ -n "$desc" ]] || return 0
  command -v gh >/dev/null 2>&1 || { log "preflight: gh not found — skipping duplicate scan."; return 0; }

  # keywords: lowercase tokens ≥ 3 chars, minus a tiny stoplist of conventional-commit noise.
  local stop=" add the for and fix feat test docs chore refactor with from into support update "
  local -a kws=()
  local tok
  for tok in $(printf '%s' "$desc" | tr 'A-Z' 'a-z' | tr -cs 'a-z0-9' ' '); do
    [[ ${#tok} -ge 3 ]] || continue
    [[ "$stop" == *" $tok "* ]] && continue
    kws+=("$tok")
  done
  [[ ${#kws[@]} -gt 0 ]] || { log "preflight: no distinctive keywords in task — skipping scan."; return 0; }

  local open_prs merged_prs blob gh_ok=1 found=0 kw
  # Capture query failures so an auth/network error is not mistaken for "no duplicates".
  open_prs="$(gh pr list --state open --limit 200 --json title --jq '.[].title' 2>/dev/null)" || gh_ok=0
  merged_prs="$(gh pr list --state merged --limit 60 --json title --jq '.[].title' 2>/dev/null)" || gh_ok=0
  if [[ $gh_ok -eq 0 ]]; then
    log "⚠️  preflight: could not query PRs (gh auth/network?) — check for duplicates manually before starting."
  fi
  blob="$(printf '%s\n%s\n' "$open_prs" "$merged_prs" | tr 'A-Z' 'a-z')"

  for kw in "${kws[@]}"; do
    # whole-word match so a short keyword like "art" doesn't spuriously match "chart".
    # kws are [a-z0-9] tokens — no regex metachars.
    if printf '%s\n' "$blob" | grep -qw -- "$kw"; then
      printf "⚠️  possible duplicate: task keyword \"%s\" appears in an open/merged PR title\n" "$kw"
      found=1
    fi
  done

  if command -v bd >/dev/null 2>&1; then
    local beads
    beads="$(bd list --status in_progress 2>/dev/null || true)"
    for kw in "${kws[@]}"; do
      if grep -qiw -- "$kw" <<<"$beads" 2>/dev/null; then
        printf "⚠️  in-progress bead matches \"%s\" — check: bd list --status in_progress\n" "$kw"
        found=1
        break
      fi
    done
    # TODO(host — epic-sibling hardening, Work-Beads Concurrency Hardening §6.3):
    # this keyword scan is the FLOOR. The stronger, semantic-dup (Window C) defense
    # is to key on the PARENT EPIC: when the bead being started has a parent, resolve
    # its siblings (`bd dep tree <id>` / `bd children <parent>`) and raise a HARD
    # warning if ANY sibling already carries an open/draft PR touching the same
    # surface. The canonical bead↔PR mapping is the PR body ("Closes <id>"); the
    # PR title and the head branch (agent/<name>/<bead-id>) carry the id
    # redundantly — so match sibling IDs against
    # `gh pr list --state open --json title,headRefName,body`.
    # Left as a host TODO because the epic/label taxonomy is project-specific.
  fi

  if [[ $found -eq 1 ]]; then
    log "If this overlaps existing work, coordinate before building. Claim your task first:"
    log "  bd update <id> --claim   # atomic claim; or: bd create \"$desc\" -t task -p 2"
    log "  # then open a DRAFT PR on your first commit:  gh pr create --draft --fill"
  else
    log "preflight: no obvious duplicate open/merged PR for \"$desc\"."
  fi
  return 0
}

# ----- subcommands -----

cmd_list() {
  log "Configured worktrees:"
  local wt="" branch="" detached="false" bare="false"
  while read -r line; do
    if [[ "$line" =~ ^worktree[[:space:]](.+) ]]; then
      if [[ -n "$wt" ]]; then
        local status="$branch"
        if [[ "$bare" == "true" ]]; then status="(bare)"; fi
        if [[ "$detached" == "true" ]]; then status="(detached HEAD)"; fi
        printf "  %-40s  %s\n" "$wt" "$status"
      fi
      wt="${BASH_REMATCH[1]}"
      branch=""
      detached="false"
      bare="false"
    elif [[ "$line" =~ ^branch[[:space:]](.+) ]]; then
      branch="${BASH_REMATCH[1]}"
      branch="${branch#refs/heads/}"
    elif [[ "$line" == "detached" ]]; then
      detached="true"
    elif [[ "$line" == "bare" ]]; then
      bare="true"
    fi
  done < <(git worktree list --porcelain)
  if [[ -n "$wt" ]]; then
    local status="$branch"
    if [[ "$bare" == "true" ]]; then status="(bare)"; fi
    if [[ "$detached" == "true" ]]; then status="(detached HEAD)"; fi
    printf "  %-40s  %s\n" "$wt" "$status"
  fi
}

# ----- concurrency lock (parallel agents may create worktrees at once) -----

SETUP_WORKTREE_LOCK_DEPTH=0
GIT_VERSION_CHECKED="false"
WORKTREE_CONFIG_ENABLED="false"

acquire_lock() {
  if [[ "$SETUP_WORKTREE_LOCK_DEPTH" -eq 0 ]]; then
    local lock_dir="$COMMON_DIR/setup-agent-worktree.lock"
    local max_retries="${SETUP_WORKTREE_MAX_RETRIES:-100}"
    local retry_interval="${SETUP_WORKTREE_RETRY_INTERVAL:-0.1}"
    local stale_timeout="${SETUP_WORKTREE_STALE_TIMEOUT:-10}"

    # Validate lock configuration: prevent command injection and ensure positive integers/numbers
    if ! [[ "$max_retries" =~ ^[1-9][0-9]*$ ]] || ! [[ "$retry_interval" =~ ^[0-9]+(\.[0-9]+)?$ ]] || ! [[ "$stale_timeout" =~ ^[1-9][0-9]*$ ]]; then
      fatal "Invalid lock configuration: max_retries='$max_retries' (must be >= 1), retry_interval='$retry_interval' (must be a positive number), stale_timeout='$stale_timeout' (must be >= 1)"
    fi
    if [[ "$retry_interval" =~ ^0+(\.0+)?$ ]]; then
      fatal "Invalid lock configuration: retry_interval='$retry_interval' (must be > 0)"
    fi

    local i=0
    local acquired="false"
    local missing_created_start=""
    while [[ "$i" -lt "$max_retries" ]]; do
      if mkdir "$lock_dir" 2>/dev/null; then
        acquired="true"
        break
      fi

      # Stale lock detection: check lock age using created_at
      if [[ -f "$lock_dir/created_at" ]]; then
        missing_created_start=""
        local created_at
        created_at="$(cat "$lock_dir/created_at" 2>/dev/null || true)"
        if [[ -n "$created_at" ]] && [[ "$created_at" =~ ^[0-9]+$ ]]; then
          local current_time
          current_time="$(date +%s 2>/dev/null || echo 0)"
          local lock_age=$((current_time - created_at))
          if [[ "$lock_age" -ge "$stale_timeout" ]]; then
            local holder_pid
            holder_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)"
            local is_alive="false"
            if [[ -n "$holder_pid" ]] && [[ "$holder_pid" =~ ^[0-9]+$ ]]; then
              if ps -p "$holder_pid" >/dev/null 2>&1; then
                if ps -p "$holder_pid" -o args= 2>/dev/null | grep -q "setup-agent-worktree"; then
                  is_alive="true"
                fi
              fi
            fi
            if [[ "$is_alive" == "false" ]]; then
              local stale_dir="${lock_dir}.stale.$$"
              if mv "$lock_dir" "$stale_dir" 2>/dev/null; then
                log "Stale lock detected (holder PID ${holder_pid:-unknown} is dead, reused, or lock is abandoned). Breaking lock..."
                rm -rf "$stale_dir" 2>/dev/null || true
              fi
            fi
          fi
        fi
      else
        # If created_at is missing, track how long we have been seeing the missing created_at file
        if [[ -z "$missing_created_start" ]]; then
          missing_created_start=$SECONDS
        fi
        if [[ $((SECONDS - missing_created_start)) -ge "$stale_timeout" ]]; then
          local stale_dir="${lock_dir}.stale.$$"
          if mv "$lock_dir" "$stale_dir" 2>/dev/null; then
            log "Stale lock detected (legacy or incomplete lock). Breaking lock..."
            rm -rf "$stale_dir" 2>/dev/null || true
          fi
          missing_created_start=""
        fi
      fi

      sleep "$retry_interval"
      i=$((i + 1))
    done

    if [[ "$acquired" == "true" ]]; then
      CLEANUP_LOCK_DIR="$lock_dir"
      echo "$$" > "$lock_dir/pid" 2>/dev/null || fatal "Failed to write PID file inside lock directory '$lock_dir'."
      echo "$0" > "$lock_dir/script_path" 2>/dev/null || true
      date +%s > "$lock_dir/created_at" 2>/dev/null || true
    else
      local wait_sec
      wait_sec="$(LC_ALL=C awk -v m="$max_retries" -v r="$retry_interval" 'BEGIN {print m * r}' 2>/dev/null || echo "10")"
      fatal "Failed to acquire lock '$lock_dir' after a configured timeout of $wait_sec seconds. Another process might be running, or the lock is stale. If no other setup-agent-worktree.sh process is running, manually remove the lock directory at '$lock_dir' and try again."
    fi
  fi
  SETUP_WORKTREE_LOCK_DEPTH=$((SETUP_WORKTREE_LOCK_DEPTH + 1))
}

release_lock() {
  if [[ "$SETUP_WORKTREE_LOCK_DEPTH" -le 0 ]]; then
    return 0
  fi
  SETUP_WORKTREE_LOCK_DEPTH=$((SETUP_WORKTREE_LOCK_DEPTH - 1))
  if [[ "$SETUP_WORKTREE_LOCK_DEPTH" -eq 0 && -n "$CLEANUP_LOCK_DIR" ]]; then
    rm -f "$CLEANUP_LOCK_DIR/pid" "$CLEANUP_LOCK_DIR/script_path" "$CLEANUP_LOCK_DIR/created_at" 2>/dev/null || true
    if rmdir "$CLEANUP_LOCK_DIR" 2>/dev/null; then
      CLEANUP_LOCK_DIR=""
    fi
  fi
}

ensure_worktree_config_enabled() {
  if [[ "$WORKTREE_CONFIG_ENABLED" == "true" ]]; then
    return 0
  fi

  if [[ "$GIT_VERSION_CHECKED" != "true" ]]; then
    # Enforce minimum Git version (2.20.0+) for worktree-local configuration support.
    local git_ver major minor
    if ! command -v git >/dev/null 2>&1; then
      fatal "git command not found in PATH"
    fi
    git_ver="$(git --version 2>/dev/null || echo "0.0.0")"

    # Parse major and minor version numbers safely using an anchored regex check
    major=""
    minor=""
    if [[ "$git_ver" =~ git[[:space:]]version[[:space:]]([0-9]+)\.([0-9]+)(\.[0-9]+)?.* ]]; then
      major="${BASH_REMATCH[1]}"
      minor="${BASH_REMATCH[2]}"
    fi

    if ! [[ "$major" =~ ^[0-9]+$ ]] || ! [[ "$minor" =~ ^[0-9]+$ ]]; then
      fatal "Failed to parse Git version (detected version string: '$git_ver'). Please ensure git is installed and working."
    fi

    if [[ "$major" -lt 2 ]] || [[ "$major" -eq 2 && "$minor" -lt 20 ]]; then
      fatal "Git version 2.20.0 or newer is required for worktree-local configuration (detected: $git_ver)"
    fi
    GIT_VERSION_CHECKED="true"
  fi

  # Lock-free fast-path: if already enabled in the config file, bypass lock acquisition
  if [[ "$(git config --file "$COMMON_DIR/config" --type=bool --get extensions.worktreeConfig 2>/dev/null || echo "false")" == "true" ]]; then
    WORKTREE_CONFIG_ENABLED="true"
    return 0
  fi

  acquire_lock

  # Perform idempotent configurations with contextual error checks.
  local cur_version
  cur_version="$(git config --file "$COMMON_DIR/config" core.repositoryFormatVersion 2>/dev/null || true)"
  if ! [[ "$cur_version" =~ ^[0-9]+$ ]]; then
    cur_version=0
  fi
  if [[ "$cur_version" -lt 1 ]]; then
    log "WARNING: Enabling extensions.worktreeConfig repository-wide. Upgrading shared repository format version to 1."
    git config --file "$COMMON_DIR/config" core.repositoryFormatVersion 1 || fatal "Failed to set core.repositoryFormatVersion 1 in shared config"
  fi

  if [[ "$(git config --file "$COMMON_DIR/config" --type=bool --get extensions.worktreeConfig 2>/dev/null || echo "false")" != "true" ]]; then
    log "Enabled extensions.worktreeConfig repository-wide"
    git config --file "$COMMON_DIR/config" extensions.worktreeConfig true || fatal "Failed to enable extensions.worktreeConfig in shared config"
  fi

  WORKTREE_CONFIG_ENABLED="true"
  release_lock
}

configure_identity() {
  local wt_path="$1"
  local name_val="$2"

  # Auto-heal: ensure extensions.worktreeConfig is enabled when configuring identity
  ensure_worktree_config_enabled

  # [HOST binding — T1-ACTOR, Work-Beads Concurrency Hardening §4.3] Per-agent
  # Beads actor. `bd update <id> --claim` is a compare-and-set KEYED ON THE ACTOR,
  # and same-actor claims are idempotent (both "win"), so every agent MUST carry a
  # DISTINCT actor or the atomic claim gives ZERO collision protection. A
  # worktree-local `.agent-env` persists that actor so every `bd` write from this
  # worktree (and a resumed session) is attributable to it. This is PERSISTENCE
  # only: a child script cannot export into the caller's shell, so the agent must
  # `set -a; . ./.agent-env; set +a` after cd-ing in. It does NOT cover the FIRST
  # claim, which the work-beads loop makes from the primary checkout BEFORE this
  # worktree exists — that claim relies on BEADS_ACTOR already exported in the
  # agent's shell (see SKILL.md Step 0).
  #
  # TODO(host): if this project ships a `bd` wrapper that pins bd's cwd to the
  # primary checkout, make the wrapper `source` this file (or otherwise PRESERVE
  # BEADS_ACTOR) so the actor survives the `cd` — env is inherited across `cd`, so
  # the wrapper only has to avoid OVERRIDING it.
  ensure_agent_env_excluded
  local env_file="$wt_path/.agent-env"

  # Read any actor already pinned by a prior run (handles the sourceable
  # `export BEADS_ACTOR='...'` form, single- or double-quoted or bare).
  local persisted_actor=""
  if [ -f "$env_file" ]; then
    persisted_actor="$(sed -n -E "s/^(export[[:space:]]+)?BEADS_ACTOR=//p" "$env_file" | head -1 | tr -d "'\"")"
  fi

  # Resolve ONE actor used for BOTH the git identity and the persisted env, so
  # blame, resume, and heartbeats never disagree:
  #   exported BEADS_ACTOR  >  already-persisted actor  >  derived agent-<name>.
  # Guard the desync the reviewer flagged: if the shell exports an actor AND the
  # worktree already pins a DIFFERENT one, fail loud rather than run git identity
  # as one actor while resume sources the other.
  if [ -n "${BEADS_ACTOR:-}" ] && [ -n "$persisted_actor" ] && [ "$BEADS_ACTOR" != "$persisted_actor" ]; then
    fatal "worktree $wt_path pins BEADS_ACTOR='$persisted_actor' but this shell exports BEADS_ACTOR='$BEADS_ACTOR' — reconcile before continuing (use the worktree matching your actor, or remove the stale worktree/.agent-env)"
  fi
  local resolved_actor="${BEADS_ACTOR:-${persisted_actor:-agent-$name_val}}"

  # git blame should attribute to the SAME identity that claims beads, so
  # user.name is the resolved actor; the email is a synthetic project-local
  # address keyed off it (sanitized to a valid local-part).
  local ident_local
  ident_local="$(printf '%s' "$resolved_actor" | tr -c 'A-Za-z0-9._-' '-')"
  git -C "$wt_path" config --worktree user.email "$ident_local@{{PROJECT_NAME}}.local" || fatal "Failed to set worktree-local email config at: $wt_path"
  git -C "$wt_path" config --worktree user.name  "$resolved_actor" || fatal "Failed to set worktree-local name config at: $wt_path"
  log "Set worktree-local git identity: $resolved_actor <$ident_local@{{PROJECT_NAME}}.local>"

  # Persist the resolved actor when nothing is pinned yet. This file gets
  # SOURCED, so the value must never be written bare: strip control characters,
  # then single-quote with embedded quotes escaped — an actor like `$(cmd)` or
  # one with spaces round-trips as literal data instead of executing or
  # corrupting the restored identity.
  if [ -z "$persisted_actor" ]; then
    local actor_val actor_sq
    actor_val="$(printf '%s' "$resolved_actor" | tr -d '[:cntrl:]')"
    actor_sq="${actor_val//\'/\'\\\'\'}"
    printf "export BEADS_ACTOR='%s'\n" "$actor_sq" >> "$env_file"
    log "Wrote BEADS_ACTOR='$actor_val' to $env_file (source it in the worktree shell)"
  fi
}

# Ensure `.worktrees/` is ignored so a project-local worktree checkout is never accidentally
# committed. Idempotent — the rule is added at most once.
#
# The exclusion goes in the repo-local `.git/info/exclude` (never tracked, never shows as a
# working-tree change) rather than the tracked `.gitignore`: the primary checkout is shared and
# treated read-only, so appending to its `.gitignore` would dirty it outside any agent's PR and
# trip doctor/clean-state checks. `git check-ignore` reads `.gitignore` FIRST, so a project that
# already committed `.worktrees/` to `.gitignore` (git-workflow step 8 recommends this) is
# detected and skipped silently — no duplicate exclude entry.
ensure_worktrees_gitignored() {
  # Use the trailing-slash directory pattern in the check so `git check-ignore` matches
  # the directory even before its contents exist (this consults .gitignore AND info/exclude).
  if ! git -C "$REPO_ROOT" check-ignore -q .worktrees/ 2>/dev/null; then
    local exclude_file="$COMMON_DIR/info/exclude"
    mkdir -p "$(dirname "$exclude_file")"
    if [ -f "$exclude_file" ] && [ -s "$exclude_file" ] && [ "$(tail -c1 "$exclude_file")" != "" ]; then
      printf '\n' >> "$exclude_file"
    fi
    printf '# Git worktrees ({{PROJECT_NAME}} parallel agents)\n.worktrees/\n' >> "$exclude_file"
    log "Ignored .worktrees/ via $exclude_file (repo-local, uncommitted). To share it, commit '.worktrees/' to .gitignore."
  fi
}

# Keep the per-worktree `.agent-env` (BEADS_ACTOR) file out of `git status` in EVERY
# worktree. info/exclude lives in the shared common git dir, so one entry covers all
# linked worktrees (a linked worktree's status is relative to its own root, where the
# `.worktrees/` pattern does NOT match). Idempotent; skipped if already ignored.
ensure_agent_env_excluded() {
  if ! git -C "$REPO_ROOT" check-ignore -q .agent-env 2>/dev/null; then
    local exclude_file="$COMMON_DIR/info/exclude"
    mkdir -p "$(dirname "$exclude_file")"
    if [ -f "$exclude_file" ] && [ -s "$exclude_file" ] && [ "$(tail -c1 "$exclude_file")" != "" ]; then
      printf '\n' >> "$exclude_file"
    fi
    printf '# Per-agent Beads actor env ({{PROJECT_NAME}} worktrees)\n.agent-env\n' >> "$exclude_file"
    log "Ignored .agent-env via $exclude_file (repo-local, uncommitted)."
  fi
}

# Seed .scaffold/identity.json inside the worktree so `scaffold observe` can attribute
# ledger events to this worktree. Best-effort: absence of .scaffold/ is created, not an error.
seed_scaffold_identity() {
  local wt="$1" name="$2"
  mkdir -p "$wt/.scaffold"
  if [ ! -f "$wt/.scaffold/identity.json" ]; then
    local identity_uuid created_at
    if command -v uuidgen >/dev/null 2>&1; then
      identity_uuid="$(uuidgen | tr 'A-Z' 'a-z')"
    elif command -v python3 >/dev/null 2>&1; then
      identity_uuid="$(python3 -c 'import uuid; print(uuid.uuid4())')"
    elif [ -r /proc/sys/kernel/random/uuid ]; then
      identity_uuid="$(cat /proc/sys/kernel/random/uuid)"
    else
      # od: strip all non-hex chars for portability across platforms
      local raw p1 p2 p3 hi
      raw="$(od -vAn -N16 -tx1 /dev/urandom | tr -dc '0-9a-f')"
      p1="${raw:0:8}" p2="${raw:8:4}" p3="4${raw:13:3}"
      hi="$(printf '%x' "$(( (16#${raw:16:1} & 3) | 8 ))")"
      identity_uuid="${p1}-${p2}-${p3}-${hi}${raw:17:3}-${raw:20:12}"
    fi
    created_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    printf '{\n  "worktree_id": "%s",\n  "worktree_label": "%s",\n  "created_at": "%s"\n}\n' \
      "$identity_uuid" "$name" "$created_at" \
      > "$wt/.scaffold/identity.json"
    log "Wrote $wt/.scaffold/identity.json (worktree_id=$identity_uuid)"
  fi
}

# Run the project's worktree setup commands (dependency installs, etc.) inside the fresh
# worktree. --install opts into this. The command block is generated at install time from
# the project's methodology config; if it is empty, this is a no-op.
run_dependency_installs() {
  local wt="$1" cd_diag=""
  # Probe cd first so a missing/unenterable worktree is reported distinctly from a
  # setup-command failure below.
  if ! cd_diag="$( (cd "$wt") 2>&1 )"; then
    fatal "cannot enter worktree $wt — cannot run setup there${cd_diag:+ ($cd_diag)}."
  fi
  log "Running worktree setup commands in $wt"
  (
    cd "$wt" || exit 1
    # --- GENERATED: worktree setup commands ---
{{WORKTREE_SETUP_COMMANDS}}
    # --- END GENERATED ---
  ) || fatal "worktree setup commands failed in $wt — re-run manually, then proceed."
}

# Resolve the remote's default branch NAME (bare, e.g. "main"), falling back to "main" when
# origin/HEAD is unset. New worktrees branch off `origin/<default>`, so hardcoding `origin/main`
# would fail `git worktree add` on a repo whose default is `master`/`trunk`/etc.
resolve_default_branch() {
  local d
  d="$(git symbolic-ref refs/remotes/origin/HEAD --short 2>/dev/null | sed 's@^origin/@@')"
  printf '%s' "${d:-main}"
}

cmd_create() {
  local name="$1" task="$2" install="${3:-0}" bead="${4:-}"
  [[ -n "$name" ]] || fatal "name is required"

  # Validate name: lowercase + digits + dashes, ≤ 40 chars.
  # Validate name early before performing any repository configuration changes.
  if ! [[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,39}$ ]]; then
    fatal "name must be lowercase alphanumeric/dashes, 1–40 chars (got: '$name')"
  fi
  # Validate the bead id before any mutation. Beads emits hierarchical child
  # ids with dots (bd-a3f8.1), so dots are allowed BETWEEN alphanumeric
  # segments — never leading, trailing, or consecutive (which would also make
  # an invalid git ref).
  if [[ -n "$bead" ]]; then
    if ! [[ "$bead" =~ ^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$ ]] || [[ "${#bead}" -gt 40 ]]; then
      fatal "--bead id must be lowercase alphanumeric/dashes with optional dot-separated segments, 1–40 chars (got: '$bead')"
    fi
    # git refuses any ref component ending in .lock — catch it here so the
    # advertised pre-mutation validation holds instead of failing mid-create.
    if [[ "$bead" == *.lock ]]; then
      fatal "--bead id may not end in '.lock' (git rejects such ref names; got: '$bead')"
    fi
  fi

  # Advisory: warn on likely-duplicate in-flight work before creating the worktree.
  # This scan runs lock-free by design to maximize throughput for concurrent read-only queries.
  preflight_scan "${task:-$name}"

  # Ensure .worktrees/ is gitignored before the checkout lands there.
  ensure_worktrees_gitignored

  # Acquire lock for the duration of the worktree creation process
  acquire_lock

  # Verify Git version and enable extensions before any git mutations occur
  ensure_worktree_config_enabled

  # The bead id rides as the branch's FINAL segment so `git branch -r` reads as
  # a live in-flight-work roster; the worktree dir and identity stay per-agent.
  local branch="agent/$name"
  [[ -n "$bead" ]] && branch="agent/$name/$bead"
  local wt="$WORKTREE_DIR/$name"

  # If the worktree already exists, just refresh tracking and exit.
  if [[ -d "$wt" ]]; then
    # Never silently rebind a live worktree to another bead's branch: finish or
    # tear down the current work first (`make prune-merged` after its PR merges).
    # A detached HEAD is equally unsafe — succeeding here would report the
    # worktree ready while it is NOT on the requested branch, so commits would
    # silently lose their bead traceability.
    local current_branch
    current_branch="$(git -C "$wt" branch --show-current 2>/dev/null || true)"
    if [[ -z "$current_branch" ]]; then
      fatal "worktree $wt is on a detached HEAD — resolve it manually (git -C $wt checkout $branch, or remove the worktree) before rerunning"
    fi
    if [[ "$current_branch" != "$branch" ]]; then
      fatal "worktree $wt is checked out on '$current_branch', not '$branch' — finish that work (or run 'make prune-merged' once its PR merges), or use a different worktree name"
    fi
    log "Worktree $wt already exists — refreshing branch tracking."
    local default_branch existing_upstream
    default_branch="origin/$(resolve_default_branch)"
    existing_upstream="$(git -C "$wt" config "branch.$branch.merge" 2>/dev/null || true)"
    if ! git -C "$wt" fetch origin 2>/dev/null; then
      warn "Could not fetch from remote repository. Tracking branch refresh might be outdated."
    else
      if [[ -z "$existing_upstream" ]]; then
        git -C "$wt" branch -u "$default_branch" "$branch" 2>/dev/null || true
      fi
    fi
    configure_identity "$wt" "$name"
    seed_scaffold_identity "$wt" "$name" || warn "could not seed .scaffold/identity.json in $wt (non-fatal)"
    log "Done. cd $wt"
    release_lock
    if [[ "$install" == "1" ]]; then
      run_dependency_installs "$wt"
    fi
    return 0
  fi

  # Migration guard (bead-suffixed branches): git stores refs as paths, so a
  # bare ref `refs/heads/agent/<name>` (from a pre-`--bead` worktree) and a
  # nested `refs/heads/agent/<name>/<bead-id>` cannot coexist — creating the
  # nested one fails with an opaque "cannot lock ref … is a directory"/D-F
  # conflict. Detect the conflicting ancestor ref up front and give a clear
  # migration instruction instead of a cryptic git error.
  if [[ "$branch" == */* ]]; then
    local ancestor="${branch%/*}" rr
    while [[ "$ancestor" == */* || "$ancestor" == "agent" ]]; do
      if git show-ref --verify --quiet "refs/heads/$ancestor"; then
        fatal "cannot create branch '$branch': the local ref '$ancestor' already exists and blocks it (a pre-upgrade $ancestor worktree/branch). Retire it first — e.g. 'make prune-merged', or 'git branch -D $ancestor' once its work is merged — then rerun."
      fi
      # A bare ancestor on the REMOTE would make the eventual push D/F-conflict
      # even when nothing local blocks creation. Catch fetched remote-tracking
      # refs here (a never-fetched remote ref still can't be seen without a
      # network call, so that residual case surfaces only at push time).
      while IFS= read -r rr; do
        [[ "${rr#*/}" == "$ancestor" ]] || continue
        fatal "cannot create branch '$branch': the remote branch '$rr' would make its push D/F-conflict (a pre-upgrade $ancestor branch on the remote). Delete it on the remote — e.g. 'git push origin --delete ${rr#*/}' once its work is merged — then rerun."
      done < <(git for-each-ref --format='%(refname:short)' refs/remotes/ 2>/dev/null)
      [[ "$ancestor" == */* ]] || break
      ancestor="${ancestor%/*}"
    done
  fi

  # If the branch exists locally but no worktree, create the worktree pointing at it.
  if git show-ref --verify --quiet "refs/heads/$branch"; then
    log "Branch $branch exists; creating worktree at $wt."
    git worktree add "$wt" "$branch"
  else
    local default_branch
    default_branch="$(resolve_default_branch)"
    log "Creating branch $branch and worktree at $wt (off origin/$default_branch)."
    # Non-fatal fetch: offline should not abort creation when a local origin/<default>
    # tracking ref already exists — `git worktree add` below falls back to it.
    git fetch origin || warn "fetch failed — proceeding with local refs"
    git worktree add -b "$branch" "$wt" "origin/$default_branch"
  fi

  # Set per-worktree git identity so blame is meaningful.
  configure_identity "$wt" "$name"
  seed_scaffold_identity "$wt" "$name" || warn "could not seed .scaffold/identity.json in $wt (non-fatal)"

  log "Done."
  log "  cd $wt"
  log "  # work normally; commit, push, open PR per your project's git workflow"
  release_lock
  # Installs run AFTER the lock is released: they write only to this worktree's own
  # dependency dirs (never shared git state) and can take minutes, so holding the
  # creation lock through them would serialize every concurrent agent's worktree setup.
  if [[ "$install" == "1" ]]; then
    run_dependency_installs "$wt"
  fi
}

# ----- entrypoint -----

case "${1:-}" in
  --help|-h|"")
    usage
    ;;
  --list)
    cmd_list
    ;;
  --preflight-only)
    task=""
    if [[ "${2:-}" == "--task" ]] && [[ $# -ge 3 ]]; then
      task="$3"
    fi
    preflight_scan "$task"
    ;;
  *)
    name="$1"
    if [[ "$name" == --* ]]; then
      fatal "unknown option: $name — a worktree <name> must come before flags (see --help)"
    fi
    shift
    task=""
    install=0
    bead=""
    # Parse trailing flags in any order: --task "<desc>", --install, --bead <id>.
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --task)
          [[ $# -ge 2 ]] || fatal "--task requires a description argument"
          [[ "$2" == --* ]] && fatal "--task requires a description argument, not another flag (got: '$2')"
          task="$2"
          shift 2
          ;;
        --install)
          install=1
          shift
          ;;
        --bead)
          [[ $# -ge 2 ]] || fatal "--bead requires a bead id argument"
          [[ "$2" == --* ]] && fatal "--bead requires a bead id argument, not another flag (got: '$2')"
          bead="$2"
          shift 2
          ;;
        *)
          fatal "unknown argument: $1 (after name '$name'); supported flags: --task \"<desc>\", --install, --bead <id>"
          ;;
      esac
    done
    cmd_create "$name" "$task" "$install" "$bead"
    ;;
esac
