# Root-only Justfile for monorepo
# Per-crate justfiles have been removed; use crate-* commands instead

set shell := ["bash", "-euo", "pipefail", "-c"]

# CI/output mode detection

ci := env("CI", "false")
output_mode := env("OUTPUT_MODE", if ci == "true" { "normal" } else { "minimal" })

# Execution wrapper: only wrap in minimal mode

exec := if output_mode == "minimal" { "tools/agent-wrap.sh " } else { "" }

# Nextest args based on mode

nextest_args := if output_mode == "minimal" { "--status-level fail --failure-output immediate --hide-progress-bar" } else if output_mode == "verbose" { "--status-level all --verbose" } else { "" }

# Nextest profile based on mode and CI

nextest_profile := if output_mode == "minimal" { "minimal" } else if ci == "true" { "ci" } else { env("NEXTEST_PROFILE", "default") }

# BEGIN:xtask:autogen justfile:mcp-servers

MCP_SERVERS := "agentic-mcp opencode-orchestrator-mcp"

# END:xtask:autogen

default: help

help:
    @echo "Workspace commands:"
    @echo "  just check            # fmt-check + clippy for entire workspace"
    @echo "  just fix              # auto-fix clippy warnings for entire workspace"
    @echo "  just test             # run tests for entire workspace"
    @echo "  just build            # build entire workspace"
    @echo "  just fmt              # format entire workspace"
    @echo "  just fmt-check        # check formatting for entire workspace"
    @echo ""
    @echo "Vendored Codex commands:"
    @echo "  just codex-check      # check vendored Codex workspace"
    @echo "  just codex-build      # build vendored Codex CLI"
    @echo "  just codex-test       # run vendored Codex tests (best-effort)"
    @echo "  just codex-run -- ... # run vendored Codex binary"
    @echo ""
    @echo "Per-crate commands:"
    @echo "  just crate-check <c>  # check a single crate by name"
    @echo "  just crate-test <c>   # test a single crate by name"
    @echo "  just crate-build <c>  # build a single crate by name"
    @echo ""
    @echo "xtask commands:"
    @echo "  just xtask-sync       # sync autogen content (CLAUDE.md, release-plz.toml, README.md, justfile)"
    @echo "  just xtask-verify     # verify metadata, policy, and file freshness"
    @echo "  just xtask-sync-check # check if sync is needed (for CI)"
    @echo "  just xtask-verify-check # full verification including generated files"
    @echo ""
    @echo "OUTPUT_MODE: minimal (local default) | normal (CI default) | verbose"

# Workspace-wide commands

check: fmt-check-just fmt-check
    {{ exec }}cargo clippy --workspace --all-targets -- -D warnings

fix:
    cargo clippy --workspace --all-targets --fix --allow-dirty

test: mcp-test
    {{ exec }}cargo nextest run --workspace --profile {{ nextest_profile }} {{ nextest_args }}

# Run integration tests (includes ignored tests that require git setup)
test-integration: mcp-test
    THOUGHTS_INTEGRATION_TESTS=1 {{ exec }}cargo nextest run --workspace --profile {{ nextest_profile }} {{ nextest_args }} -- --include-ignored

build:
    {{ exec }}cargo build --workspace

codex-check:
    cd vendor/codex/codex-rs && cargo check -p codex-cli --all-targets

codex-build:
    cd vendor/codex/codex-rs && cargo build -p codex-cli

codex-test:
    cd vendor/codex/codex-rs && RUST_MIN_STACK=8388608 cargo nextest run --no-fail-fast

codex-run *args:
    cd vendor/codex/codex-rs && cargo run --bin codex -- {{ args }}

fmt:
    {{ exec }}cargo +nightly fmt --all
    {{ exec }}taplo fmt $(git ls-files '*.toml' ':!:vendor/**')

fmt-check:
    {{ exec }}cargo +nightly fmt --all -- --check
    {{ exec }}taplo fmt --check $(git ls-files '*.toml' ':!:vendor/**')

# Security audit with cargo-deny
deny:
    {{ exec }}cargo deny check

# Check justfile formatting
fmt-check-just:
    @just --fmt --check --unstable

# Per-crate commands
crate-check crate:
    {{ exec }}cargo +nightly fmt -p {{ crate }} -- --check
    {{ exec }}cargo clippy -p {{ crate }} --all-targets -- -D warnings

crate-test crate:
    {{ exec }}cargo nextest run --profile {{ nextest_profile }} {{ nextest_args }} -E 'package({{ crate }})'

crate-build crate:
    {{ exec }}cargo build -p {{ crate }}

crate-run crate:
    cargo run -p {{ crate }}

# xtask commands

xtask-sync:
    {{ exec }}cargo run -p xtask -- sync

xtask-verify:
    {{ exec }}cargo run -p xtask -- verify

xtask-sync-check:
    {{ exec }}cargo run -p xtask -- sync --check

xtask-verify-check:
    {{ exec }}cargo run -p xtask -- verify --check

# Endpoint coverage commands for opencode-rs SDK
endpoint-coverage:
    {{ exec }}cargo run -p xtask -- endpoint-coverage

endpoint-coverage-check:
    {{ exec }}cargo run -p xtask -- endpoint-coverage --check

endpoint-coverage-json:
    {{ exec }}cargo run -p xtask -- endpoint-coverage --json

# Utility commands

thoughts_sync:
    {{ exec }}thoughts sync

# Copy a file
cp src dst:
    {{ exec }}cp "{{ src }}" "{{ dst }}"

# Remove a file
rm path:
    rm -f "{{ path }}"

# Create a directory (with parents)
mkdir path:
    {{ exec }}mkdir -p "{{ path }}"

# Set file executable
chmod-x path:
    chmod +x "{{ path }}"

# ------------------------------------------------------------------------------
# Git - Read-only Navigation (for agents without shell access)
#
# These recipes expose safe, read-only git inspection commands via just.
# All commands use --no-pager to avoid interactive hangs.
# ------------------------------------------------------------------------------

# git-context: Repo snapshot - root, branch/HEAD, remotes, status, last N commits. n defaults to 5.
git-context n="5":
    #!/usr/bin/env bash
    set -euo pipefail

    N="{{ n }}"
    if ! [[ "$N" =~ ^[0-9]+$ ]]; then
      echo "Invalid commit count '$N', defaulting to 5." >&2
      N=5
    fi

    if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
      echo "Not inside a git repository." >&2
      exit 1
    fi

    echo "Repo root:"
    git rev-parse --show-toplevel
    echo

    current_branch="$(git symbolic-ref --short -q HEAD 2>/dev/null || true)"
    if [ -n "$current_branch" ]; then
      echo "Branch: $current_branch"
    else
      head_sha="$(git rev-parse --short HEAD 2>/dev/null || true)"
      if [ -n "$head_sha" ]; then
        echo "HEAD detached at $head_sha"
      else
        echo "No commits yet."
      fi
    fi
    echo

    echo "Remotes:"
    git --no-pager remote -v || true
    echo

    echo "Status (short):"
    git --no-pager status --porcelain -b || true
    echo

    echo "Recent commits:"
    git --no-pager log --decorate --oneline -n "$N" 2>/dev/null || echo "(no commits yet)"

# git-log: Commit history, optionally scoped to a path. n defaults to 20.
git-log n="20" path="":
    #!/usr/bin/env bash
    set -euo pipefail

    N="{{ n }}"
    if ! [[ "$N" =~ ^[0-9]+$ ]]; then
      echo "Invalid commit count '$N', defaulting to 20." >&2
      N=20
    fi

    P="{{ path }}"

    if [ -z "$P" ]; then
      git --no-pager log --decorate --graph --date=short -n "$N" \
        --pretty=format:'%C(auto)%h %ad %an%d %s' 2>/dev/null \
        || echo "(no commits yet)"
    else
      git --no-pager log --decorate --graph --date=short -n "$N" \
        --pretty=format:'%C(auto)%h %ad %an%d %s' -- "$P" 2>/dev/null \
        || echo "(no commits for path or path invalid)"
    fi

# git-diff: Diffs for staged/working tree. area: both|working|staged|head. format: stat|patch|name-only|name-status.
git-diff area="both" format="stat" path="":
    #!/usr/bin/env bash
    set -euo pipefail

    AREA="{{ area }}"
    FORMAT="{{ format }}"
    P="{{ path }}"

    case "$AREA" in
      both|working|staged|head) ;;
      *) echo "Invalid area: '$AREA'. Allowed: both|working|staged|head" >&2; exit 2 ;;
    esac

    fmt_flags=()
    case "$FORMAT" in
      stat)        fmt_flags+=(--stat) ;;
      patch)       fmt_flags+=(-p) ;;
      name-only)   fmt_flags+=(--name-only) ;;
      name-status) fmt_flags+=(--name-status) ;;
      *) echo "Invalid format: '$FORMAT'. Allowed: stat|patch|name-only|name-status" >&2; exit 2 ;;
    esac

    run_diff() {
      local cached="$1"
      if [ -z "$P" ]; then
        if [ "$cached" = "cached" ]; then
          git --no-pager diff --cached "${fmt_flags[@]}" || true
        else
          git --no-pager diff "${fmt_flags[@]}" || true
        fi
      else
        if [ "$cached" = "cached" ]; then
          git --no-pager diff --cached "${fmt_flags[@]}" -- "$P" || true
        else
          git --no-pager diff "${fmt_flags[@]}" -- "$P" || true
        fi
      fi
    }

    case "$AREA" in
      working)
        echo "=== Unstaged changes (working tree) ==="
        run_diff "working"
        ;;
      staged)
        echo "=== Staged changes (index) ==="
        run_diff "cached"
        ;;
      both)
        echo "=== Staged changes (index) ==="
        run_diff "cached"
        echo
        echo "=== Unstaged changes (working tree) ==="
        run_diff "working"
        ;;
      head)
        if [ -z "$P" ]; then
          git --no-pager diff "${fmt_flags[@]}" HEAD || true
        else
          git --no-pager diff "${fmt_flags[@]}" HEAD -- "$P" || true
        fi
        ;;
    esac

# git-blame: Annotate a file to see who last modified each line. Optional line range (start <= end).
git-blame file start="" end="":
    #!/usr/bin/env bash
    set -euo pipefail

    FILE="{{ file }}"
    START="{{ start }}"
    END="{{ end }}"

    if [ -z "$FILE" ]; then
      echo "Usage: just git-blame <file> [start] [end]" >&2
      exit 2
    fi

    range_args=()
    if [ -n "$START" ] || [ -n "$END" ]; then
      if ! [[ "$START" =~ ^[0-9]+$ ]] || ! [[ "$END" =~ ^[0-9]+$ ]]; then
        echo "Line range must be integers: start and end" >&2
        exit 2
      fi
      if [ "$START" -gt "$END" ]; then
        echo "Invalid range: start ($START) > end ($END)" >&2
        exit 2
      fi
      range_args=(-L "${START},${END}")
    fi

    git --no-pager blame -w "${range_args[@]}" -- "$FILE" || true

# git-show: Show commit details (ref only) or file contents at a ref (ref + path).
git-show ref path="":
    #!/usr/bin/env bash
    set -euo pipefail

    REF="{{ ref }}"
    P="{{ path }}"

    if [ -z "$REF" ]; then
      echo "Usage: just git-show <ref> [path]" >&2
      exit 2
    fi

    if [ -z "$P" ]; then
      git --no-pager show --stat --decorate --pretty=fuller "$REF" || true
    else
      git --no-pager show "${REF}:${P}" || true
    fi

# git-files: List tracked files, optionally filtered by whitespace-separated git pathspec patterns (paths containing spaces not supported).
git-files patterns="":
    #!/usr/bin/env bash
    set -euo pipefail

    if [ -z "{{ patterns }}" ]; then
      git --no-pager ls-files
      exit 0
    fi

    # Disable glob expansion so patterns are passed literally to git
    set -f
    git --no-pager ls-files -- {{ patterns }}

# ------------------------------------------------------------------------------
# Schema Generation
# ------------------------------------------------------------------------------

# Generate agentic.schema.json from Rust types
schema-generate:
    cargo run -p agentic-bin -- config schema > agentic.schema.json

# ------------------------------------------------------------------------------
# Git - Write Operations (for agents without shell access)
#
# These recipes enforce git-aware move/remove semantics via git mv / git rm.
# They fail loudly on unsuitable git state instead of falling back to plain mv/rm.
# ------------------------------------------------------------------------------

# git-mv: Move or rename a tracked path with git-aware semantics. Optionally create dst parent first.
git-mv src dst mkdir_parents="true":
    #!/usr/bin/env bash
    set -euo pipefail

    SRC="{{ src }}"
    DST="{{ dst }}"
    MKDIR_PARENTS="{{ mkdir_parents }}"

    if [ -z "$SRC" ] || [ -z "$DST" ]; then
      echo "Usage: just git-mv <src> <dst> [mkdir_parents]" >&2
      exit 2
    fi

    case "$MKDIR_PARENTS" in
      true|false) ;;
      *) echo "Invalid mkdir_parents: '$MKDIR_PARENTS'. Allowed: true|false" >&2; exit 2 ;;
    esac

    if [ "$MKDIR_PARENTS" = "true" ]; then
      mkdir -p "$(dirname "$DST")"
    fi

    git mv -- "$SRC" "$DST"

# git-rm: Remove a tracked path with git-aware semantics. force: true|false. recursive: auto|true|false.
git-rm path force="false" recursive="auto":
    #!/usr/bin/env bash
    set -euo pipefail

    P="{{ path }}"
    FORCE="{{ force }}"
    RECURSIVE="{{ recursive }}"

    if [ -z "$P" ]; then
      echo "Usage: just git-rm <path> [force] [recursive]" >&2
      exit 2
    fi

    case "$FORCE" in
      true|false) ;;
      *) echo "Invalid force: '$FORCE'. Allowed: true|false" >&2; exit 2 ;;
    esac

    case "$RECURSIVE" in
      auto|true|false) ;;
      *) echo "Invalid recursive: '$RECURSIVE'. Allowed: auto|true|false" >&2; exit 2 ;;
    esac

    rm_flags=()
    if [ "$FORCE" = "true" ]; then
      rm_flags+=(-f)
    fi

    case "$RECURSIVE" in
      true)
        rm_flags+=(-r)
        ;;
      auto)
        if [ -d "$P" ]; then
          rm_flags+=(-r)
        fi
        ;;
    esac

    git rm "${rm_flags[@]}" -- "$P"

# ------------------------------------------------------------------------------
# MCP Inspector Recipes
# ------------------------------------------------------------------------------
# Interactive MCP Inspector for troubleshooting
# Usage:
#   just mcp-inspector              # default: tools/list method

# just mcp-inspector resources/list
mcp-inspector method="tools/list":
    #!/usr/bin/env bash
    set -euo pipefail
    cargo build -p agentic-mcp
    BIN="./target/debug/agentic-mcp"
    if [ ! -x "$BIN" ]; then
      echo "agentic-mcp binary not found: $BIN" >&2
      exit 1
    fi
    echo "Launching MCP Inspector with method: {{ method }}"
    npx -y @modelcontextprotocol/inspector --cli --transport stdio --method "{{ method }}" "$BIN"

# CI-friendly MCP schema validation (validates all MCP servers in MCP_SERVERS)
mcp-test:
    {{ exec }}tools/mcp-validate.sh {{ MCP_SERVERS }}

# ------------------------------------------------------------------------------
# PR Description Management
# ------------------------------------------------------------------------------

# Update PR body with autogen content, preserving human notes outside markers
pr-update-autogen number body_file:
    #!/usr/bin/env bash
    set -euo pipefail

    PR="{{ number }}"
    AUTOGEN_FILE="{{ body_file }}"

    if [ -z "$PR" ] || [ -z "$AUTOGEN_FILE" ]; then
      echo "Usage: just pr-update-autogen <pr-number> <autogen-body-file>" >&2
      exit 2
    fi

    if [ ! -f "$AUTOGEN_FILE" ]; then
      echo "Autogen body file not found: $AUTOGEN_FILE" >&2
      exit 2
    fi

    tmp_dir="$(mktemp -d)"
    trap 'rm -rf "$tmp_dir"' EXIT

    current="$tmp_dir/current.md"
    merged="$tmp_dir/merged.md"

    gh pr view "$PR" --json body -q .body > "$current"

    perl -0777 -we '
      use strict;
      use warnings;

      my ($current_path, $autogen_path) = @ARGV;

      local $/;
      open my $cfh, "<", $current_path or die "Failed to read current PR body: $!\n";
      my $body = <$cfh>;
      close $cfh;

      open my $afh, "<", $autogen_path or die "Failed to read autogen body file: $!\n";
      my $autogen = <$afh>;
      close $afh;

      $body    //= "";
      $body = "" if $body eq "null";  # gh outputs literal "null" for empty PR body
      $autogen //= "";
      $autogen =~ s/\n+\z//;

      my $begin = "<!-- BEGIN:describe_pr:autogen pr-description -->";
      my $end   = "<!-- END:describe_pr:autogen -->";
      my $replacement = "$begin\n$autogen\n$end";

      (my $trim = $body) =~ s/\s+//g;
      if ($trim eq "") {
        print "## My Notes (preserved)\n\n";
        print "<!-- Add any scratch notes, todos, and observations here. This section is preserved when /describe_pr runs. -->\n\n";
        print $replacement . "\n";
        exit 0;
      }

      my $has_begin = $body =~ /<!--\s*BEGIN:describe_pr:autogen\s+pr-description\s*-->/;
      my $has_end   = $body =~ /<!--\s*END:describe_pr:autogen\s*-->/;

      if (!$has_begin && !$has_end) {
        warn "WARNING: No describe_pr autogen markers found in PR body; appending autogenerated block to the end.\n";
        $body .= "\n" unless $body =~ /\n\z/;
        print $body . "\n" . $replacement . "\n";
        exit 0;
      }

      if ($has_begin xor $has_end) {
        die "ERROR: Malformed describe_pr autogen markers: expected both BEGIN and END. Refusing to update PR body.\n";
      }

      my $pattern = qr/(?s)<!--\s*BEGIN:describe_pr:autogen\s+pr-description\s*-->.*?<!--\s*END:describe_pr:autogen\s*-->/;
      if ($body !~ $pattern) {
        die "ERROR: Found describe_pr markers but could not match a BEGIN..END block (possible ordering issue). Refusing to update.\n";
      }

      $body =~ s/$pattern/$replacement/;
      print $body;
    ' "$current" "$AUTOGEN_FILE" > "$merged"

    gh pr edit "$PR" --body-file "$merged"
