#!/usr/bin/env bash
set -euo pipefail

afk_source_path="${BASH_SOURCE[0]}"
while [[ -L "$afk_source_path" ]]; do
  afk_source_dir="$(cd -P "$(dirname "$afk_source_path")" && pwd)"
  afk_linked_path="$(readlink "$afk_source_path")"
  if [[ "$afk_linked_path" == /* ]]; then
    afk_source_path="$afk_linked_path"
  else
    afk_source_path="${afk_source_dir}/${afk_linked_path}"
  fi
done
afk_script_dir="$(cd -P "$(dirname "$afk_source_path")" && pwd)"

usage() {
  cat <<'USAGE'
Usage:
  scripts/afk-workflow run [--concurrency 2] [--max-waves 20] [--base main] [--engine codex] [--afk-label afk] [--dry-run]

Runs the full AFK issue workflow:

1. Pick up to N queued GitHub issues.
2. Claim each issue so another worker does not pick it.
3. Run one AgentRail issue execution per issue in an isolated git worktree.
4. Find the PR opened for the issue.
5. Review the PR in a fresh context.
6. Convert machine-readable review findings into new review-fix issues.
7. When review has no fix issues, prepare and merge the PR.
8. Repeat until the queue is empty or max waves is reached.

Defaults:
  --concurrency 2
  --max-waves 20
  --base main
  --engine codex
  --afk-label afk
  --queue-labels review-fix,ready-for-agent

Environment:
  AFK_WORKFLOW_MOCK_QUEUE  Test-only TSV queue: number, title, url, labels_csv.
  AFK_WORKFLOW_REVIEW     Set to 0 to skip review. Default: 1.

Examples:
  scripts/afk-workflow run
  scripts/afk-workflow run --concurrency 2 --max-waves 5
  scripts/afk-workflow run --dry-run
USAGE
}

die() {
  echo "afk-workflow: $*" >&2
  exit 1
}

need() {
  command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

script_path() {
  local root="$1"
  local script_name="$2"

  if [[ -x "${root}/scripts/${script_name}" ]]; then
    printf '%s\n' "${root}/scripts/${script_name}"
    return 0
  fi

  if [[ -x "${root}/templates/scripts/${script_name}" ]]; then
    printf '%s\n' "${root}/templates/scripts/${script_name}"
    return 0
  fi

  if [[ -x "${afk_script_dir}/${script_name}" ]]; then
    printf '%s\n' "${afk_script_dir}/${script_name}"
    return 0
  fi

  die "missing workflow script: ${script_name}"
}

agentrail_path() {
  local root="$1"

  if [[ -x "${root}/scripts/agentrail" ]]; then
    printf '%s\n' "${root}/scripts/agentrail"
    return 0
  fi

  if command -v agentrail >/dev/null 2>&1; then
    command -v agentrail
    return 0
  fi

  die "missing AgentRail CLI: expected scripts/agentrail in source checkout or agentrail on PATH"
}

ensure_agentrail_state_ready() {
  local root="$1"
  local state_file="${root}/.agentrail/state.json"

  [[ -f "$state_file" ]] || die "missing AgentRail state at .agentrail/state.json; run agentrail install --target ${root}"
  command -v node >/dev/null 2>&1 || die "node is required to read AgentRail state"

  node - "$state_file" <<'NODE'
const fs = require("fs");
const statePath = process.argv[2];

let state;
try {
  state = JSON.parse(fs.readFileSync(statePath, "utf8"));
} catch (error) {
  console.error(`invalid AgentRail state at .agentrail/state.json: ${error.message}`);
  process.exit(1);
}

const workflow = state.workflow && typeof state.workflow === "object" ? state.workflow : {};
const run = workflow.activeRun;
if (run && typeof run === "object") {
  const issue = run.targetIssue ?? workflow.activeIssue ?? "unknown";
  console.error(`active AgentRail run exists for issue #${issue}; inspect .agentrail/state.json before starting AFK`);
  if (run.runDir) console.error(`run dir: ${run.runDir}`);
  if (run.metadataFile) console.error(`metadata: ${run.metadataFile}`);
  if (workflow.nextSuggestedAction) console.error(`next action: ${workflow.nextSuggestedAction}`);
  process.exit(1);
}

console.log("AgentRail state: ready");
NODE
}

prepare_worktree_agentrail_state() {
  local root="$1"
  local worktree="$2"

  local source_state="${root}/.agentrail/state.json"
  local target_state="${worktree}/.agentrail/state.json"
  mkdir -p "${worktree}/.agentrail"

  if [[ ! -f "$target_state" ]]; then
    cp "$source_state" "$target_state"
  fi

  if [[ -f "${root}/.agentrail/config.json" && ! -f "${worktree}/.agentrail/config.json" ]]; then
    cp "${root}/.agentrail/config.json" "${worktree}/.agentrail/config.json"
  fi
}

sanitized_agent_exec() {
  env \
    -u CLAUDECODE -u CLAUDE_CODE_SESSION_ID -u CLAUDE_CODE_ENTRYPOINT \
    -u CLAUDE_AGENT_SDK_VERSION -u CLAUDE_CODE_EXECPATH -u CLAUDE_EFFORT \
    -u AI_AGENT \
    -u CODEX_SESSION -u CODEX_SANDBOX \
    -u CURSOR_SESSION -u CURSOR_AGENT \
    "$@"
}

agent_command_for_engine() {
  local engine="$1"

  case "$engine" in
    codex)
      printf '%s\n' "${RALPH_AGENT_COMMAND:-codex exec --sandbox danger-full-access -}"
      ;;
    claude)
      printf '%s\n' "${RALPH_AGENT_COMMAND:-claude -p --dangerously-skip-permissions}"
      ;;
    *)
      die "unsupported engine: ${engine}"
      ;;
  esac
}

repo_root() {
  git rev-parse --show-toplevel
}

ensure_clean_tree() {
  local status
  status="$(git status --porcelain -- ':!.agentrail' ':!.afk-workflow')"
  [[ -z "$status" ]] || die "working tree is dirty; commit, stash, or clean changes before running AFK workflow"
}

ensure_label() {
  local name="$1"
  local color="$2"
  local description="$3"

  gh label list --limit 200 --json name --jq '.[].name' | grep -Fxq "$name" && return 0
  gh label create "$name" --color "$color" --description "$description" --force >/dev/null
}

ensure_workflow_labels() {
  local afk_label="$1"

  ensure_label "$afk_label" "5319E7" "Issue is approved for unattended AFK agent execution."
  ensure_label "afk-in-progress" "BFDADC" "Issue is currently claimed by the AFK workflow."
  ensure_label "pr-reviewed" "C5DEF5" "Implementation PR has completed automated review."
  ensure_label "review-fix" "D93F0B" "Follow-up issue created from PR review feedback."
  ensure_label "memory-suggestion" "FBCA04" "Suggested project memory update for human review."
  ensure_label "ready-for-agent" "0E8A16" "Issue is ready for an agent to implement."
}

queue_issues() {
  local labels_csv="$1"
  local limit="$2"
  local afk_label="$3"

  if [[ -n "${AFK_WORKFLOW_MOCK_QUEUE:-}" ]]; then
    local emitted=0
    local number title url mock_labels
    while IFS=$'\t' read -r number title url mock_labels; do
      [[ -n "$number" ]] || continue
      if [[ ",${mock_labels}," == *",${afk_label},"* ]]; then
        printf '%s\t%s\t%s\n' "$number" "$title" "$url"
        emitted=$((emitted + 1))
        [[ "$emitted" -lt "$limit" ]] || break
      fi
    done <<< "$AFK_WORKFLOW_MOCK_QUEUE"
    return 0
  fi

  local labels=()
  IFS=',' read -r -a labels <<< "$labels_csv"

  local candidate_limit=$((limit * 10))
  [[ "$candidate_limit" -lt 20 ]] && candidate_limit=20

  local emitted=0
  local number title url
  while IFS=$'\t' read -r number title url; do
    [[ -n "$number" ]] || continue
    if [[ -z "$(detect_pr_for_issue "$number")" ]]; then
      printf '%s\t%s\t%s\n' "$number" "$title" "$url"
      emitted=$((emitted + 1))
      [[ "$emitted" -lt "$limit" ]] || break
    fi
  done < <(
    local label
    for label in "${labels[@]}"; do
      gh issue list \
        --state open \
        --label "$afk_label" \
        --label "$label" \
        --search "sort:created-asc -label:afk-in-progress" \
        --limit "$candidate_limit" \
        --json number,title,url \
        | jq -r 'sort_by(.number)[] | [.number, .title, .url] | @tsv'
    done | awk -F '\t' 'NF >= 3 && !seen[$1]++'
  )
}

claim_issue() {
  local issue="$1"

  gh issue edit "$issue" --add-label "afk-in-progress" >/dev/null
}

mark_issue_reviewed() {
  local issue="$1"
  local labels_csv="$2"

  local labels=()
  IFS=',' read -r -a labels <<< "$labels_csv"

  gh issue edit "$issue" --remove-label "afk-in-progress" >/dev/null 2>&1 || true
  local label
  for label in "${labels[@]}"; do
    gh issue edit "$issue" --remove-label "$label" >/dev/null 2>&1 || true
  done
  gh issue edit "$issue" --add-label "pr-reviewed" >/dev/null
}

release_issue_to_queue() {
  local issue="$1"

  gh issue edit "$issue" --remove-label "afk-in-progress" >/dev/null 2>&1 || true
}

detect_pr_for_issue() {
  local issue="$1"

  gh pr list \
    --state open \
    --limit 100 \
    --json number,title,url,body,createdAt \
    --jq "map(select(((.body // \"\") | contains(\"#${issue}\")) or ((.title // \"\") | contains(\"#${issue}\")))) | sort_by(.createdAt) | last | .number // empty"
}

queue_review_only_issues() {
  local labels_csv="$1"
  local limit="$2"
  local afk_label="$3"

  # Find issues that already have an open PR but haven't been reviewed yet.
  # These are partial runs where execute completed but verify/review did not.
  local labels=()
  IFS=',' read -r -a labels <<< "$labels_csv"

  local candidate_limit=$((limit * 10))
  [[ "$candidate_limit" -lt 20 ]] && candidate_limit=20

  local emitted=0
  local number title url
  while IFS=$'\t' read -r number title url; do
    [[ -n "$number" ]] || continue
    # Only pick issues that DO have an open PR (opposite of queue_issues)
    local pr
    pr="$(detect_pr_for_issue "$number")"
    if [[ -n "$pr" ]]; then
      printf '%s\t%s\t%s\t%s\n' "$number" "$title" "$url" "$pr"
      emitted=$((emitted + 1))
      [[ "$emitted" -lt "$limit" ]] || break
    fi
  done < <(
    local label
    for label in "${labels[@]}"; do
      gh issue list \
        --state open \
        --label "$afk_label" \
        --label "$label" \
        --search "sort:created-asc -label:afk-in-progress -label:pr-reviewed" \
        --limit "$candidate_limit" \
        --json number,title,url \
        | jq -r 'sort_by(.number)[] | [.number, .title, .url] | @tsv'
    done | awk -F '\t' 'NF >= 3 && !seen[$1]++'
  )
}

extract_review_fix_json() {
  local review_file="$1"

  awk '
    /BEGIN_REVIEW_FIX_ISSUES_JSON/ { in_json = 1; next }
    /END_REVIEW_FIX_ISSUES_JSON/ { in_json = 0; exit }
    in_json { print }
  ' "$review_file"
}

review_fix_issue_count() {
  local review_file="$1"
  local json

  json="$(extract_review_fix_json "$review_file")"
  if [[ -z "$json" ]]; then
    echo "review for ${review_file} did not include review fix JSON markers" >&2
    return 1
  fi

  if ! jq -e '.fix_issues | type == "array"' >/dev/null <<< "$json"; then
    echo "review for ${review_file} did not include a valid fix_issues array" >&2
    return 1
  fi

  jq '.fix_issues | length' <<< "$json"
}

create_review_fix_issues() {
  local source_issue="$1"
  local pr="$2"
  local review_file="$3"
  local afk_label="$4"

  local json
  json="$(extract_review_fix_json "$review_file")"
  [[ -n "$json" ]] || return 0

  if ! jq -e '.fix_issues | type == "array"' >/dev/null <<< "$json"; then
    echo "review for PR #${pr} did not include valid fix issue JSON; leaving review output at ${review_file}" >&2
    return 0
  fi

  local count
  count="$(jq '.fix_issues | length' <<< "$json")"
  [[ "$count" -gt 0 ]] || return 0

  local i title body severity file
  for ((i = 0; i < count; i++)); do
    title="$(jq -r ".fix_issues[$i].title" <<< "$json")"
    body="$(jq -r ".fix_issues[$i].body" <<< "$json")"
    severity="$(jq -r ".fix_issues[$i].severity // \"review-fix\"" <<< "$json")"
    file="$(jq -r ".fix_issues[$i].file // \"\"" <<< "$json")"

    gh issue create \
      --title "[review-fix] PR #${pr}: ${title}" \
      --body "$(cat <<BODY
Created from automated PR review.

Source issue: #${source_issue}
Source PR: #${pr}
Severity: ${severity}
File: ${file}

${body}

Run this through AgentRail as a small follow-up issue.
BODY
)" \
      --label "review-fix" \
      --label "$afk_label" \
      --label "ready-for-agent" >/dev/null
  done
}

create_memory_suggestion_issues() {
  local source_issue="$1"
  local pr="$2"
  local review_file="$3"
  local afk_label="$4"

  local json
  json="$(extract_review_fix_json "$review_file")"
  [[ -n "$json" ]] || return 0

  if ! jq -e '.memory_suggestions | type == "array"' >/dev/null <<< "$json"; then
    return 0
  fi

  local count
  count="$(jq '.memory_suggestions | length' <<< "$json")"
  [[ "$count" -gt 0 ]] || return 0

  local i kind title target_file source body
  for ((i = 0; i < count; i++)); do
    kind="$(jq -r ".memory_suggestions[$i].kind // \"failure-pattern\"" <<< "$json")"
    title="$(jq -r ".memory_suggestions[$i].title" <<< "$json")"
    target_file="$(jq -r ".memory_suggestions[$i].target_file // \"docs/memory/failure-patterns.md\"" <<< "$json")"
    source="$(jq -r ".memory_suggestions[$i].source // \"PR #${pr} review\"" <<< "$json")"
    body="$(jq -r ".memory_suggestions[$i].body" <<< "$json")"

    gh issue create \
      --title "[memory-suggestion] PR #${pr}: ${title}" \
      --body "$(cat <<BODY
Created from automated PR review.

Source issue: #${source_issue}
Source PR: #${pr}
Memory kind: ${kind}
Target file: ${target_file}
Source: ${source}

Proposed memory entry:

\`\`\`md
## ${title}

- kind: ${kind}
- source: ${source}
- confidence: verified
- created_at: $(date +%Y-%m-%d)
- expires_at:

${body}
\`\`\`

Review this suggestion before adding it to project memory. Do not store secrets, customer data, private personal data, generic advice, or unsourced assumptions.
BODY
)" \
      --label "memory-suggestion" \
      --label "$afk_label" \
      --label "ready-for-agent" >/dev/null
  done
}

write_review_artifacts_for_merge() {
  local root="$1"
  local pr="$2"
  local review_file="$3"

  local review_dir="${root}/.worktrees/pr-${pr}/.local"
  [[ -d "$review_dir" ]] || die "missing PR review worktree artifacts at ${review_dir}"

  cat >"${review_dir}/review.md" <<EOF_REVIEW
A) TL;DR recommendation

READY FOR /prepare-pr. Automated AFK review produced no fix issues.

B) What changed and what is good?

See the captured automated review below.

C) Security findings

No blocking security findings were reported by the automated review.

D) What is the PR intent? Is this the most optimal implementation?

The automated review found no merge-blocking implementation concerns.

E) Concerns or questions (actionable)

None reported.

F) Tests

See the PR body and prepare-run gate output.

G) Docs status

Not applicable unless prepare-run or the PR body identifies required docs work.

H) Changelog

Required by prepare-run.

I) Follow ups (optional)

None.

J) Suggested PR comment (optional)

Reviewed by AFK automation with no fix issues.

Captured automated review:

$(cat "$review_file")
EOF_REVIEW

  cat >"${review_dir}/review.json" <<'EOF_JSON'
{
  "recommendation": "READY FOR /prepare-pr",
  "findings": [],
  "tests": {
    "ran": [],
    "gaps": [],
    "result": "pass"
  },
  "docs": "not_applicable",
  "changelog": "required"
}
EOF_JSON
}

merge_reviewed_pr() {
  local root="$1"
  local pr="$2"
  local review_file="$3"
  local logs_dir="$4"

  # Try the full pr script pipeline if rg is available; otherwise fall back
  # to a direct gh pr merge which is sufficient for AFK auto-merges.
  if command -v rg >/dev/null 2>&1; then
    local pr_runner
    pr_runner="$(script_path "$root" "pr")"

    "$pr_runner" review-init "$pr" >"${logs_dir}/pr-${pr}-merge-review-init.log" 2>&1
    write_review_artifacts_for_merge "$root" "$pr" "$review_file"
    "$pr_runner" review-validate-artifacts "$pr" >"${logs_dir}/pr-${pr}-merge-review-validate.log" 2>&1
    "$pr_runner" prepare-run "$pr" >"${logs_dir}/pr-${pr}-prepare.log" 2>&1
    "$pr_runner" merge-run "$pr" >"${logs_dir}/pr-${pr}-merge.log" 2>&1
  else
    echo "rg (ripgrep) not found; using direct merge for PR #${pr}" >&2
    afk_direct_merge "$root" "$pr" "$review_file" "$logs_dir"
  fi
}

afk_direct_merge() {
  local root="$1"
  local pr="$2"
  local review_file="$3"
  local logs_dir="$4"

  local pr_json
  pr_json="$(gh pr view "$pr" --json number,title,state,isDraft,headRefOid)"
  local pr_title pr_state is_draft head_sha
  pr_title="$(jq -r '.title' <<< "$pr_json")"
  pr_state="$(jq -r '.state' <<< "$pr_json")"
  is_draft="$(jq -r '.isDraft' <<< "$pr_json")"
  head_sha="$(jq -r '.headRefOid' <<< "$pr_json")"

  [[ "$pr_state" == "OPEN" ]] || die "PR #${pr} is not open (state=${pr_state})"
  [[ "$is_draft" != "true" ]] || die "PR #${pr} is a draft; cannot merge"

  # Post review summary as a PR comment before merging
  gh pr comment "$pr" --body "$(cat <<COMMENT
**AFK automated review — no fix issues found.**

Merging via AFK direct merge (full pr pipeline unavailable: rg not installed).

<details>
<summary>Review output</summary>

$(head -200 "$review_file" 2>/dev/null || echo "Review file not available")

</details>
COMMENT
)" >"${logs_dir}/pr-${pr}-review-comment.log" 2>&1 || true

  # Squash merge — try immediate first, fall back to --auto if branch protection blocks it
  if ! gh pr merge "$pr" \
    --squash \
    --match-head-commit "$head_sha" \
    --subject "${pr_title} (#${pr})" \
    --body "Merged via AFK automated review. No fix issues found." \
    >"${logs_dir}/pr-${pr}-merge.log" 2>&1; then
    # Branch protection may require checks to pass first; use --auto to queue
    if grep -q "requirements" "${logs_dir}/pr-${pr}-merge.log" 2>/dev/null; then
      echo "Branch protection active; enabling auto-merge for PR #${pr}" >&2
      if ! gh pr merge "$pr" \
        --auto \
        --squash \
        --subject "${pr_title} (#${pr})" \
        --body "Merged via AFK automated review. No fix issues found." \
        >>"${logs_dir}/pr-${pr}-merge.log" 2>&1; then
        cat "${logs_dir}/pr-${pr}-merge.log" >&2
        die "gh pr merge --auto failed for PR #${pr}"
      fi
      echo "Auto-merge enabled for PR #${pr}; will merge when checks pass"
      return 0
    fi
    cat "${logs_dir}/pr-${pr}-merge.log" >&2
    die "gh pr merge failed for PR #${pr}"
  fi

  # Clean up remote branch after merge (ignore failures)
  local head_ref
  head_ref="$(gh pr view "$pr" --json headRefName --jq .headRefName 2>/dev/null || true)"
  if [[ -n "$head_ref" && "$head_ref" != "main" ]]; then
    git push origin --delete "$head_ref" 2>/dev/null || true
  fi

  # Wait for merge to finalize
  local state
  state="$(gh pr view "$pr" --json state --jq .state)"
  if [[ "$state" != "MERGED" ]]; then
    echo "Waiting for merge to finalize (state=${state})..." >&2
    local i
    for i in $(seq 1 30); do
      sleep 5
      state="$(gh pr view "$pr" --json state --jq .state)"
      [[ "$state" != "MERGED" ]] || break
    done
  fi

  [[ "$state" == "MERGED" ]] || die "PR #${pr} merge did not finalize (state=${state})"
  echo "PR #${pr} merged successfully"
}

worker_run() {
  local root="$1"
  local run_dir="$2"
  local slot="$3"
  local issue="$4"
  local title="$5"
  local base="$6"
  local engine="$7"
  local review_enabled="$8"
  local queue_labels="$9"
  local afk_label="${10}"

  local worktree="${run_dir}/worktrees/slot-${slot}-issue-${issue}"
  local logs_dir="${run_dir}/logs"
  mkdir -p "$logs_dir"

  echo "worker ${slot}: issue #${issue} ${title}"

  local agentrail_runner
  agentrail_runner="$(agentrail_path "$root")"

  git -C "$root" fetch origin "$base"
  git -C "$root" worktree add --detach "$worktree" "origin/${base}" >/dev/null
  "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status running --issue "$issue" --run-dir "$run_dir" --base "$base" --slot "$slot"
  prepare_worktree_agentrail_state "$root" "$worktree"

  (
    cd "$worktree"
    local agent_command
    agent_command="$(agent_command_for_engine "$engine")"

    set +e
    sanitized_agent_exec env AGENTRAIL_ALLOW_SOURCE_RUN=1 \
      "$agentrail_runner" run issue "$issue" --agent "$engine" --target "$worktree" --command "$agent_command" \
        >"${logs_dir}/issue-${issue}-agentrail.log" 2>&1
    local agentrail_status=$?
    set -e
    if [[ "$agentrail_status" -ne 0 ]]; then
      "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status failed --issue "$issue" --run-dir "$run_dir" --base "$base" --slot "$slot"
      return "$agentrail_status"
    fi

    local pr
    pr="$(detect_pr_for_issue "$issue")"
    if [[ -z "$pr" ]]; then
      "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status failed --issue "$issue" --run-dir "$run_dir" --base "$base" --slot "$slot"
      die "could not find an open PR for issue #${issue}"
    fi

    echo "worker ${slot}: issue #${issue} opened PR #${pr}"
    "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status completed --issue "$issue" --pr "$pr" --run-dir "$run_dir" --base "$base" --slot "$slot"

    if [[ "$review_enabled" == "1" ]]; then
      local review_output
      local review_log
      review_output="${logs_dir}/pr-${pr}-review.md"
      review_log="${logs_dir}/pr-${pr}-review.log"

      if ! "$agentrail_runner" internal review-pr --pr "$pr" --engine "$engine" --output "$review_output" --machine-readable >"$review_log" 2>&1; then
        echo "worker ${slot}: review failed for PR #${pr}; see ${review_log}" >&2
        "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status failed --issue "$issue" --pr "$pr" --run-dir "$run_dir" --base "$base" --slot "$slot"
        return 1
      fi

      if [[ ! -s "$review_output" ]]; then
        echo "worker ${slot}: review for PR #${pr} did not produce ${review_output}" >&2
        "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status failed --issue "$issue" --pr "$pr" --run-dir "$run_dir" --base "$base" --slot "$slot"
        return 1
      fi

      local fix_issue_count
      if ! fix_issue_count="$(review_fix_issue_count "$review_output")"; then
        echo "worker ${slot}: review for PR #${pr} did not produce valid machine-readable fix issue output; see ${review_output}" >&2
        "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status failed --issue "$issue" --pr "$pr" --run-dir "$run_dir" --base "$base" --slot "$slot"
        return 1
      fi

      create_review_fix_issues "$issue" "$pr" "$review_output" "$afk_label"
      create_memory_suggestion_issues "$issue" "$pr" "$review_output" "$afk_label"

      if [[ "$fix_issue_count" -eq 0 ]]; then
        echo "worker ${slot}: review found no fix issues for PR #${pr}; preparing and merging"
        if ! merge_reviewed_pr "$root" "$pr" "$review_output" "$logs_dir"; then
          "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status failed --issue "$issue" --pr "$pr" --run-dir "$run_dir" --base "$base" --slot "$slot"
          return 1
        fi
        "$agentrail_runner" internal worktree mark --target "$root" --path "$worktree" --status merged --issue "$issue" --pr "$pr" --run-dir "$run_dir" --base "$base" --slot "$slot"
        cd "$root"
        "$agentrail_runner" cleanup --target "$root" --merged
      else
        echo "worker ${slot}: review created ${fix_issue_count} fix issue(s) for PR #${pr}; skipping merge"
      fi
    fi

    cd "$root"
    mark_issue_reviewed "$issue" "$queue_labels"
  )
}

worker_review_only() {
  local root="$1"
  local run_dir="$2"
  local slot="$3"
  local issue="$4"
  local title="$5"
  local pr="$6"
  local base="$7"
  local engine="$8"
  local queue_labels="$9"
  local afk_label="${10}"

  local logs_dir="${run_dir}/logs"
  mkdir -p "$logs_dir"

  echo "worker ${slot}: review-only issue #${issue} PR #${pr}"

  local agentrail_runner
  agentrail_runner="$(agentrail_path "$root")"

  local review_output review_log
  review_output="${logs_dir}/pr-${pr}-review.md"
  review_log="${logs_dir}/pr-${pr}-review.log"

  (
    cd "$root"

    # Remove stale worktrees that have the PR branch checked out, which would
    # block review-pr from running git switch on the PR head branch.
    local head_ref
    head_ref="$(gh pr view "$pr" --json headRefName --jq .headRefName 2>/dev/null || true)"
    if [[ -n "$head_ref" ]]; then
      local wt_path wt_branch
      while IFS= read -r wt_path; do
        [[ -n "$wt_path" ]] || continue
        wt_branch="$(git -C "$wt_path" branch --show-current 2>/dev/null || true)"
        if [[ "$wt_branch" == "$head_ref" ]]; then
          echo "worker ${slot}: removing stale worktree at ${wt_path} (branch ${head_ref})"
          git worktree remove --force "$wt_path" 2>/dev/null || true
        fi
      done < <(git worktree list --porcelain | awk '/^worktree / { print substr($0, 10) }')
      git worktree prune 2>/dev/null || true
    fi

    if ! "$agentrail_runner" internal review-pr --pr "$pr" --engine "$engine" --output "$review_output" --machine-readable >"$review_log" 2>&1; then
      echo "worker ${slot}: review failed for PR #${pr}; see ${review_log}" >&2
      return 1
    fi

    if [[ ! -s "$review_output" ]]; then
      echo "worker ${slot}: review for PR #${pr} did not produce ${review_output}" >&2
      return 1
    fi

    local fix_issue_count
    if ! fix_issue_count="$(review_fix_issue_count "$review_output")"; then
      echo "worker ${slot}: review for PR #${pr} did not produce valid machine-readable fix issue output; see ${review_output}" >&2
      return 1
    fi

    create_review_fix_issues "$issue" "$pr" "$review_output" "$afk_label"
    create_memory_suggestion_issues "$issue" "$pr" "$review_output" "$afk_label"

    if [[ "$fix_issue_count" -eq 0 ]]; then
      echo "worker ${slot}: review found no fix issues for PR #${pr}; preparing and merging"
      if ! merge_reviewed_pr "$root" "$pr" "$review_output" "$logs_dir"; then
        return 1
      fi
    else
      echo "worker ${slot}: review created ${fix_issue_count} fix issue(s) for PR #${pr}; skipping merge"
    fi

    mark_issue_reviewed "$issue" "$queue_labels"
  )
}

main() {
  local command="${1:-}"
  [[ -n "$command" ]] || { usage; exit 1; }
  shift || true

  local concurrency="2"
  local max_waves="20"
  local base="main"
  local engine="codex"
  local queue_labels="review-fix,ready-for-agent"
  local afk_label="afk"
  local dry_run="0"
  local review_enabled="${AFK_WORKFLOW_REVIEW:-1}"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --concurrency)
        concurrency="${2:-}"
        [[ -n "$concurrency" ]] || die "--concurrency requires a number"
        shift 2
        ;;
      --max-waves)
        max_waves="${2:-}"
        [[ -n "$max_waves" ]] || die "--max-waves requires a number"
        shift 2
        ;;
      --base)
        base="${2:-}"
        [[ -n "$base" ]] || die "--base requires a branch"
        shift 2
        ;;
      --engine)
        engine="${2:-}"
        [[ -n "$engine" ]] || die "--engine requires a value"
        shift 2
        ;;
      --queue-labels)
        queue_labels="${2:-}"
        [[ -n "$queue_labels" ]] || die "--queue-labels requires comma-separated labels"
        shift 2
        ;;
      --afk-label)
        afk_label="${2:-}"
        [[ -n "$afk_label" ]] || die "--afk-label requires a label"
        shift 2
        ;;
      --dry-run)
        dry_run="1"
        shift
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "unknown option: $1"
        ;;
    esac
  done

  [[ "$command" == "run" ]] || { usage; exit 1; }
  [[ "$concurrency" =~ ^[0-9]+$ && "$concurrency" -gt 0 ]] || die "--concurrency must be a positive number"
  [[ "$max_waves" =~ ^[0-9]+$ && "$max_waves" -gt 0 ]] || die "--max-waves must be a positive number"
  [[ "$engine" == "codex" || "$engine" == "claude" ]] || die "unsupported engine: $engine"

  local root
  root="$(repo_root)"
  cd "$root"
  agentrail_path "$root" >/dev/null
  ensure_agentrail_state_ready "$root"

  if [[ "$dry_run" == "0" ]]; then
    need git
    need gh
    need jq
    [[ "$engine" != "codex" ]] || need codex
    ensure_clean_tree
    ensure_workflow_labels "$afk_label"
  fi

  local run_dir="${root}/.afk-workflow/$(date +%Y%m%d-%H%M%S)"
  if [[ "$dry_run" == "0" ]]; then
    mkdir -p "$run_dir"
  fi

  local wave
  for ((wave = 1; wave <= max_waves; wave++)); do

    # --- Review-only pass: pick up issues with open PRs that need review ---
    if [[ "$review_enabled" == "1" ]]; then
      local review_queue
      review_queue="$(queue_review_only_issues "$queue_labels" "$concurrency" "$afk_label")"

      if [[ -n "$review_queue" ]]; then
        local review_pids=()
        local review_slot=0
        while IFS=$'\t' read -r issue title url pr; do
          [[ -n "$issue" ]] || continue
          review_slot=$((review_slot + 1))
          echo "wave ${wave}: review-only issue #${issue} PR #${pr} ${title}"

          if [[ "$dry_run" == "1" ]]; then
            echo "dry-run: would review PR #${pr} for issue #${issue}"
            continue
          fi

          claim_issue "$issue"
          (
            if ! worker_review_only "$root" "$run_dir" "$review_slot" "$issue" "$title" "$pr" "$base" "$engine" "$queue_labels" "$afk_label"; then
              release_issue_to_queue "$issue"
              exit 1
            fi
          ) &
          review_pids+=("$!")
        done <<< "$review_queue"

        if [[ ${#review_pids[@]} -gt 0 ]]; then
          local rpid
          local review_failed=0
          for rpid in "${review_pids[@]}"; do
            if ! wait "$rpid"; then
              review_failed=1
            fi
          done
          # Review failures are non-fatal; continue to the regular queue
          if [[ "$review_failed" != "0" ]]; then
            echo "wave ${wave}: some review-only workers failed; continuing"
          fi
        fi
      fi
    fi

    # --- Regular pass: pick issues that need implementation ---
    local queue
    queue="$(queue_issues "$queue_labels" "$concurrency" "$afk_label")"

    if [[ -z "$queue" ]]; then
      echo "NO MORE TASKS"
      exit 0
    fi

    local pids=()
    local slot=0
    while IFS=$'\t' read -r issue title url; do
      [[ -n "$issue" ]] || continue
      slot=$((slot + 1))
      echo "wave ${wave}: selected issue #${issue} ${title}"
      echo "wave ${wave}: ${url}"

      if [[ "$dry_run" == "1" ]]; then
        echo "dry-run: would claim issue #${issue}"
        echo "dry-run: would run AgentRail issue execution for issue #${issue}"
        echo "dry-run: would review PR for issue #${issue}"
        continue
      fi

      claim_issue "$issue"
      (
        if ! worker_run "$root" "$run_dir" "$slot" "$issue" "$title" "$base" "$engine" "$review_enabled" "$queue_labels" "$afk_label"; then
          release_issue_to_queue "$issue"
          exit 1
        fi
      ) &
      pids+=("$!")
    done <<< "$queue"

    if [[ ${#pids[@]} -gt 0 ]]; then
      local pid
      local failed=0
      for pid in "${pids[@]}"; do
        if ! wait "$pid"; then
          failed=1
        fi
      done
      [[ "$failed" == "0" ]] || exit 1
    fi
  done

  echo "Reached max waves (${max_waves}) before queue was empty."
}

main "$@"
