Ctrl K
ComunidadGuías…

Justfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import? "~/.justfile"

set shell := ["bash", "-euo", "pipefail", "-c"]
set windows-shell := ["pwsh", "-NoLogo", "-Command"]

# Shared role defaults; exported overrides survive nested recipes and agent sessions.
# See docs/agents/model-configuration.md for Bash usage and manual fleet selection.
implement_model := env("MINA_DEV_IMPLEMENT_MODEL", "gpt-6.1-sol")
implement_effort := env("MINA_DEV_IMPLEMENT_EFFORT", "high")
plan_model := env("MINA_DEV_PLAN_MODEL", "gpt-6.1-sol")
plan_effort := env("MINA_DEV_PLAN_EFFORT", "high")
fleet_model := env("MINA_DEV_FLEET_MODEL", "claude-opus-5-5")
fleet_effort := env("MINA_DEV_FLEET_EFFORT", "high")
fleet_codex_model := env("MINA_DEV_FLEET_CODEX_MODEL", "gpt-6.1-sol")
fleet_codex_effort := env("MINA_DEV_FLEET_CODEX_EFFORT", "medium")
review_codex_model := env("MINA_DEV_REVIEW_CODEX_MODEL", "gpt-6.1-sol")
review_codex_effort := env("MINA_DEV_REVIEW_CODEX_EFFORT", "high")
review_claude_model := env("MINA_DEV_REVIEW_CLAUDE_MODEL", "claude-opus-5-5")
review_claude_effort := env("MINA_DEV_REVIEW_CLAUDE_EFFORT", "high")
aggregate_model := env("MINA_DEV_AGGREGATE_MODEL", "gpt-6-luna")
aggregate_effort := env("MINA_DEV_AGGREGATE_EFFORT", "xhigh")
validate_model := env("MINA_DEV_VALIDATE_MODEL", "gpt-6.1-sol")
validate_effort := env("MINA_DEV_VALIDATE_EFFORT", "high")
garden_model := env("MINA_DEV_GARDEN_MODEL", "gpt-6.1-sol")
garden_effort := env("MINA_DEV_GARDEN_EFFORT", "high")

guardian_model := env("MINA_DEV_GUARDIAN_MODEL", "gpt-6.1-sol")
guardian_effort := env("MINA_DEV_GUARDIAN_EFFORT", "high")

export MINA_DEV_IMPLEMENT_MODEL := implement_model
export MINA_DEV_IMPLEMENT_EFFORT := implement_effort
export MINA_DEV_PLAN_MODEL := plan_model
export MINA_DEV_PLAN_EFFORT := plan_effort
export MINA_DEV_FLEET_MODEL := fleet_model
export MINA_DEV_FLEET_EFFORT := fleet_effort
export MINA_DEV_FLEET_CODEX_MODEL := fleet_codex_model
export MINA_DEV_FLEET_CODEX_EFFORT := fleet_codex_effort
export MINA_DEV_REVIEW_CODEX_MODEL := review_codex_model
export MINA_DEV_REVIEW_CODEX_EFFORT := review_codex_effort
export MINA_DEV_REVIEW_CLAUDE_MODEL := review_claude_model
export MINA_DEV_REVIEW_CLAUDE_EFFORT := review_claude_effort
export MINA_DEV_AGGREGATE_MODEL := aggregate_model
export MINA_DEV_AGGREGATE_EFFORT := aggregate_effort
export MINA_DEV_VALIDATE_MODEL := validate_model
export MINA_DEV_VALIDATE_EFFORT := validate_effort
export MINA_DEV_GARDEN_MODEL := garden_model
export MINA_DEV_GARDEN_EFFORT := garden_effort
export MINA_DEV_GUARDIAN_MODEL := guardian_model
export MINA_DEV_GUARDIAN_EFFORT := guardian_effort
agent_model_thinking_levels := implement_model + "/" + implement_effort + "\n" + review_claude_model + "/" + review_claude_effort + "\n" + review_claude_model + "/xhigh\n" + review_codex_model + "/medium\n" + review_codex_model + "/" + review_codex_effort + "\n" + garden_model + "/" + garden_effort + "\n" + garden_model + "/xhigh"

[private]
@default:
    just --list

# Install local development hooks.
[group('dev-tooling')]
init:
    command -v mise >/dev/null || { echo "missing required tool: mise" >&2; exit 1; }
    mise install
    just frontend-install
    mise exec -- prek install --hook-type pre-commit

# Validate GitHub Actions workflow syntax.
# TODO: Move actionlint back to go.mod's tool block after actionlint supports yaml/v4 rc.6 (https://github.com/rhysd/actionlint/pull/730); the versioned go run isolates actionlint v1.7.12's yaml/v4 rc.3 dependency from golangci-lint v2.13.2's gosec dependency on rc.6.
[group('dev-tooling')]
workflow-check:
    go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12

# Format Go source files.
[group('dev-tooling')]
fmt:
    go fmt ./...

# Check Go source formatting.
[group('dev-tooling')]
fmt-check:
    files="$(git ls-files '*.go' | while IFS= read -r file; do [ -f "$file" ] && gofmt -l "$file"; done)"; if [ -n "$files" ]; then printf 'Go files need formatting:\n%s\n' "$files" >&2; exit 1; fi

# Normalize prose in eligible tracked Markdown documents.
[group('dev-tooling')]
[working-directory: 'frontend']
prose-fmt:
    #!/usr/bin/env bash
    set -euo pipefail

    paths=()
    while IFS= read -r -d '' path; do
        case "$path" in
            LICENSE.md|README.md|SCOPE.md|docs/plans/*) continue ;;
        esac
        [[ -e "../$path" ]] || continue
        [[ -L "../$path" ]] && continue
        paths+=("../$path")
    done < <(git -C .. ls-files -z '*.md')
    ((${#paths[@]})) || exit 0
    mise exec -- pnpm exec prettier --write --prose-wrap never "${paths[@]}"

# Apply Go source fixes.
[group('dev-tooling')]
fix:
    go fix ./...

# Run static analysis checks.
[group('dev-tooling')]
lint:
    go tool golangci-lint run ./...
    go run ./internal/tools/archlint

# Regenerate OpenAPI server and client code.
[group('codegen')]
openapi:
    go tool oapi-codegen -config api/oapi-codegen.yaml api/openapi.yaml
    go tool oapi-codegen -config api/oapi-codegen-httpclient.yaml api/openapi.yaml
    go run ./internal/tools/surfacegen

# Validate OpenAPI client-surface exposure decisions.
[group('codegen')]
surface-check:
    go run ./internal/tools/surfacegen -verify

# Validate OpenAPI and generated code freshness.
[group('codegen')]
openapi-check: surface-check
    go run github.com/getkin/kin-openapi/cmd/validate api/openapi.yaml
    tmpdir="$(mktemp -d)"; trap 'rm -rf "$tmpdir"' EXIT; awk -v output="$tmpdir/server/openapi.gen.go" '/^output:/ { print "output: " output; next } { print }' api/oapi-codegen.yaml > "$tmpdir/oapi-codegen.yaml"; mkdir -p "$tmpdir/server"; go tool oapi-codegen -config "$tmpdir/oapi-codegen.yaml" api/openapi.yaml; cmp -s "$tmpdir/server/openapi.gen.go" internal/httpapi/openapi/openapi.gen.go || { echo 'generated OpenAPI server output is stale; run `just openapi`' >&2; diff -u internal/httpapi/openapi/openapi.gen.go "$tmpdir/server/openapi.gen.go" >&2; exit 1; }; awk -v output="$tmpdir/client/openapi.gen.go" '/^output:/ { print "output: " output; next } { print }' api/oapi-codegen-httpclient.yaml > "$tmpdir/oapi-codegen-httpclient.yaml"; mkdir -p "$tmpdir/client"; go tool oapi-codegen -config "$tmpdir/oapi-codegen-httpclient.yaml" api/openapi.yaml; cmp -s "$tmpdir/client/openapi.gen.go" internal/httpclient/openapi.gen.go || { echo 'generated OpenAPI client output is stale; run `just openapi`' >&2; diff -u internal/httpclient/openapi.gen.go "$tmpdir/client/openapi.gen.go" >&2; exit 1; }

# Regenerate the pristine current accounting-schema DDL artifact.
[group('codegen')]
accounting-schema:
    go run ./internal/tools/accountingschema

# Verify the checked-in accounting-schema DDL artifact is current.
[group('codegen')]
accounting-schema-check:
    main_ref="$(git rev-parse --verify 'main^{commit}' 2>/dev/null || git rev-parse --verify 'origin/main^{commit}' 2>/dev/null)" || { echo 'cannot verify migration immutability: main ref is missing' >&2; exit 1; }; changed_migrations="$(git diff --no-renames --diff-filter=MD --name-only "$main_ref" -- internal/store/migrations/)"; [[ -z "$changed_migrations" ]] || { echo 'migrations already on main are immutable; add a new migration instead:' >&2; echo "$changed_migrations" >&2; exit 1; }; tmpdir="$(mktemp -d)"; trap 'rm -rf "$tmpdir"' EXIT; go run ./internal/tools/accountingschema -output "$tmpdir/schema.sql"; cmp -s internal/services/accountingschema/schema.sql "$tmpdir/schema.sql" || { echo 'generated accounting schema is stale; run `just accounting-schema`' >&2; diff -u internal/services/accountingschema/schema.sql "$tmpdir/schema.sql" >&2; exit 1; }

# Tidy Go module files.
[group('dev-tooling')]
tidy:
    go mod tidy

# Install the frontend package dependencies.
[group('frontend')]
[working-directory: 'frontend']
frontend-install:
    mise exec -- pnpm install

# Format frontend source files.
[group('frontend')]
[working-directory: 'frontend']
frontend-fmt:
    mise exec -- pnpm run format

# Check frontend source formatting.
[group('frontend')]
[working-directory: 'frontend']
frontend-fmt-check:
    mise exec -- pnpm run format:check

# Run frontend static analysis checks.
[group('frontend')]
[working-directory: 'frontend']
frontend-lint:
    mise exec -- pnpm run lint

# Run frontend TypeScript compiler checks.
[group('frontend')]
[working-directory: 'frontend']
frontend-typecheck:
    mise exec -- pnpm run typecheck

# Build frontend assets.
[group('frontend')]
[working-directory: 'frontend']
frontend-build:
    mise exec -- pnpm run build

# Start the Vite dev server against a running Mina backend.
[group('frontend')]
[working-directory: 'frontend']
frontend-dev backend_url="http://127.0.0.1:8080":
    MINA_FRONTEND_BACKEND_URL={{ quote(backend_url) }} mise exec -- pnpm run dev

# Run all frontend static checks.
[group('frontend')]
frontend-check: frontend-openapi-check frontend-fmt-check frontend-lint frontend-typecheck

# Check Docker builder versions against development tool declarations.
[group('docker')]
docker-version-check:
    scripts/check-docker-versions.sh

# Check that a remote image index includes every supported deployment platform.
[group('docker')]
docker-manifest-check image:
    #!/usr/bin/env bash
    set -euo pipefail

    image={{ quote(image) }}
    [ -n "$image" ] || { echo "usage: just docker-manifest-check IMAGE" >&2; exit 2; }
    manifest="$(docker buildx imagetools inspect "$image")"
    grep -Eq 'Platform:[[:space:]]+linux/amd64' <<<"$manifest" || { echo "$image is missing linux/amd64" >&2; exit 1; }
    grep -Eq 'Platform:[[:space:]]+linux/arm64' <<<"$manifest" || { echo "$image is missing linux/arm64" >&2; exit 1; }
    printf '%s includes linux/amd64 and linux/arm64\n' "$image"

# Run the real Docker image and Compose lifecycle test.
[group('docker')]
test-docker:
    scripts/docker-service-test.sh

# Regenerate frontend REST client code.
[group('codegen')]
[working-directory: 'frontend']
frontend-openapi:
    mise exec -- pnpm exec openapi-ts -f openapi-ts.config.ts --no-log-file

# Verify frontend REST client generated code freshness.
[group('codegen')]
frontend-openapi-check:
    tmpdir="$(mktemp -d)"; trap 'rm -rf "$tmpdir"' EXIT; cd frontend; mise exec -- pnpm exec openapi-ts -f openapi-ts.config.ts -o "$tmpdir/generated" --no-log-file >/dev/null; diff -qr src/api/generated "$tmpdir/generated" >/dev/null || { echo 'generated frontend OpenAPI client output is stale; run `just frontend-openapi`' >&2; diff -ru src/api/generated "$tmpdir/generated" >&2; exit 1; }

# Build the mina binary without rebuilding frontend assets.
[group('dev-tooling')]
build-go:
    test -f internal/webui/dist/index.html || { echo 'missing embedded frontend assets; run `just frontend-build` or `just build`' >&2; exit 1; }
    mkdir -p bin
    go build -buildvcs=false -ldflags="-X github.com/mishamsk/mina/internal/runtime.buildCommitHash=$(git rev-parse --verify HEAD 2>/dev/null || true) -X github.com/mishamsk/mina/internal/runtime.buildRepositoryURL=$(git remote get-url origin 2>/dev/null || true)" -o bin/mina ./cmd/mina

# Build frontend assets and the mina binary.
[group('dev-tooling')]
build: frontend-build build-go

[doc('''Start the REST API in the background; pass -p to persist data in build/dev/mina.db.
Pass --db <path> to use a specific Mina database.
Pass --demo to seed deterministic demo data and enable login plus API authentication.''')]
[group('demo')]
dev mode="" extra="" value="": build
    #!/usr/bin/env bash
    set -euo pipefail

    dev_dir="build/dev"
    pid_file="$dev_dir/mina.pid"
    stdout_log="$dev_dir/stdout.log"
    stderr_log="$dev_dir/stderr.log"
    access_log="$dev_dir/access.log"
    tailscale_port_file="$dev_dir/tailscale-port"
    db_path="$dev_dir/mina.db"
    config_path="$dev_dir/config.toml"
    auth_path="$dev_dir/auth.toml"
    default_port=8080
    persist=false
    demo=false
    custom_db_path=""
    expect_db_path=false

    port_in_use() {
        (exec 3<>"/dev/tcp/127.0.0.1/$1") >/dev/null 2>&1
    }

    select_dev_port() {
        if ! port_in_use "$default_port"; then
            echo "$default_port"
            return
        fi

        for port in $(seq 49152 65535); do
            if ! port_in_use "$port"; then
                echo "$port"
                return
            fi
        done

        echo "no free high port found" >&2
        exit 1
    }

    for arg in {{ quote(mode) }} {{ quote(extra) }} {{ quote(value) }}; do
        if [ "$expect_db_path" = true ]; then
            if [ -z "$arg" ]; then
                echo "usage: just dev [-p | --db <path>] [--demo]" >&2
                exit 2
            fi
            custom_db_path="$arg"
            expect_db_path=false
            continue
        fi
        case "$arg" in
            "")
                ;;
            "-p")
                persist=true
                ;;
            "--demo")
                demo=true
                ;;
            "--db")
                expect_db_path=true
                ;;
            *)
                echo "usage: just dev [-p | --db <path>] [--demo]" >&2
                exit 2
                ;;
        esac
    done
    if [ "$expect_db_path" = true ] || { [ "$persist" = true ] && [ -n "$custom_db_path" ]; }; then
        echo "usage: just dev [-p | --db <path>] [--demo]" >&2
        exit 2
    fi

    mkdir -p "$dev_dir"
    if [ -f "$pid_file" ]; then
        pid="$(cat "$pid_file")"
        if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
            echo "mina already running at http://127.0.0.1:8080 with pid $pid" >&2
            exit 1
        fi
        rm -f "$pid_file"
        if [ -f "$tailscale_port_file" ]; then
            tailscale_port="$(cat "$tailscale_port_file")"
            tailscale serve --https="$tailscale_port" off
            rm -f "$tailscale_port_file"
        fi
    fi

    port="$(select_dev_port)"
    : > "$stdout_log"
    : > "$stderr_log"
    mina_command=(./bin/mina)
    serve_args=(serve --host 127.0.0.1 --port "$port" --access-log "$access_log")
    if [ "$persist" = true ]; then
        serve_args+=(--db "$db_path" --yes)
    elif [ -n "$custom_db_path" ]; then
        serve_args+=(--db "$custom_db_path" --yes)
    fi
    if [ "$demo" = true ]; then
        printf 'auth_file = "auth.toml"\n' > "$config_path"
        chmod 0600 "$config_path"
        install -m 0600 scripts/fixtures/demo-auth.toml "$auth_path"
        mina_command+=(--config-file "$config_path")
        serve_args+=(--demo)
    fi
    nohup "${mina_command[@]}" "${serve_args[@]}" > "$stdout_log" 2> "$stderr_log" &
    pid="$!"
    echo "$pid" > "$pid_file"
    disown "$pid"

    for _ in {1..50}; do
        if grep -q "listening http://127.0.0.1:$port" "$stdout_log"; then
            if ! tailscale serve --https="$port" --bg "http://127.0.0.1:$port"; then
                kill -TERM "$pid" 2>/dev/null || true
                rm -f "$pid_file"
                echo "failed to serve Mina over Tailscale" >&2
                exit 1
            fi
            echo "$port" > "$tailscale_port_file"
            echo "mina listening at http://127.0.0.1:$port with pid $pid"
            if [ "$port" != "$default_port" ]; then
                echo "default port $default_port was busy; selected high port $port"
            fi
            if [ "$demo" = true ]; then
                echo "login: admin@local / password"
                echo "api token: mina-token"
            fi
            echo "logs: $stdout_log $stderr_log $access_log"
            exit 0
        fi
        if ! kill -0 "$pid" 2>/dev/null; then
            rm -f "$pid_file"
            echo "mina failed to start; see $stdout_log and $stderr_log" >&2
            exit 1
        fi
        sleep 0.1
    done

    echo "mina did not report readiness; see $stdout_log and $stderr_log" >&2
    exit 1

# Stop the REST API demo server.
[group('demo')]
dev-kill:
    #!/usr/bin/env bash
    set -euo pipefail

    pid_file="build/dev/mina.pid"
    tailscale_port_file="build/dev/tailscale-port"

    stop_tailscale_serve() {
        if [ ! -f "$tailscale_port_file" ]; then
            return
        fi

        tailscale_port="$(cat "$tailscale_port_file")"
        tailscale serve --https="$tailscale_port" off
        rm -f "$tailscale_port_file"
    }

    print_detected_mina_serve() {
        detected_pids="$(pgrep -f '(^|.*/)mina serve([[:space:]]|$)' || true)"
        if [ -z "$detected_pids" ]; then
            echo "no mina serve processes detected"
            return
        fi

        echo "detected mina serve processes:"
        while IFS= read -r detected_pid; do
            command_line="$(ps -p "$detected_pid" -o command= 2>/dev/null || true)"
            listen_addrs="$(lsof -nP -a -p "$detected_pid" -iTCP -sTCP:LISTEN 2>/dev/null | awk 'NR > 1 { print $9 }' | paste -sd, - || true)"
            if [ -n "$listen_addrs" ]; then
                echo "  pid $detected_pid listening $listen_addrs: $command_line"
            else
                echo "  pid $detected_pid: $command_line"
            fi
        done <<< "$detected_pids"
        echo "to stop all detected mina serve processes: kill -TERM $(echo "$detected_pids" | paste -sd' ' -)"
    }

    if [ ! -f "$pid_file" ]; then
        stop_tailscale_serve
        echo "no background mina pid file found"
        print_detected_mina_serve
        exit 0
    fi

    pid="$(cat "$pid_file")"
    if [ -z "$pid" ]; then
        rm -f "$pid_file"
        stop_tailscale_serve
        echo "removed empty mina pid file"
        print_detected_mina_serve
        exit 0
    fi

    if ! kill -0 "$pid" 2>/dev/null; then
        rm -f "$pid_file"
        stop_tailscale_serve
        echo "removed stale mina pid file for pid $pid"
        print_detected_mina_serve
        exit 0
    fi

    kill -TERM "$pid"
    for _ in {1..50}; do
        if ! kill -0 "$pid" 2>/dev/null; then
            rm -f "$pid_file"
            stop_tailscale_serve
            echo "stopped mina pid $pid"
            exit 0
        fi
        sleep 0.1
    done

    kill -KILL "$pid"
    rm -f "$pid_file"
    stop_tailscale_serve
    echo "force-stopped mina pid $pid"

# Run all Go tests.
[group('dev-tooling')]
test:
    go test ./...

# Compare test inventories with the existing main worktree; use --worktree for uncommitted changes.
[group('dev-tooling')]
[positional-arguments]
test-diff *args:
    #!/usr/bin/env bash
    set -euo pipefail
    exec go run ./internal/tools/testdiff "$@"

# Inventory app tests without executing test bodies (also used against the main worktree).
[private]
test-list-go root:
    @cd {{ quote(root) }} && go test -json -list '^Test' ./internal/apptest/runtime

# Inventory browser tests without building Mina or installing/launching browsers.
[private]
test-list-frontend root:
    @cd {{ quote(root) }}/frontend && mise exec -- pnpm exec playwright test --list --reporter=json --forbid-only --pass-with-no-tests

# Run focused concurrent app tests with the Go race detector.
[group('dev-tooling')]
test-race-concurrency:
    go test -race ./internal/apptest/runtime -run '^TestConcurrent' -count=1

# Run REST integration tests.
[group('dev-tooling')]
test-integration: frontend-build
    go test -tags=integration ./cmd/mina -run TestIntegrationScripts -count=1

# Run frontend browser end-to-end tests against the embedded UI.
[group('dev-tooling')]
[working-directory: 'frontend']
[positional-arguments]
test-frontend-e2e *playwright_args: frontend-install build
    #!/usr/bin/env bash
    set -euo pipefail

    playwright_install_args=(chromium webkit)
    if [[ "$(uname -s)" == Linux ]]; then
        playwright_install_args=(--with-deps "${playwright_install_args[@]}")
    fi
    mise exec -- pnpm exec playwright install "${playwright_install_args[@]}"
    if [[ $# -eq 1 && $1 == "" ]]; then
        set --
    fi
    while IFS= read -r mina_environment_name; do
        if [[ $mina_environment_name == MINA_* ]]; then
            unset "$mina_environment_name"
        fi
    done < <(compgen -e)
    # Bypass `pnpm exec`: its SIGINT forwarding can exit Playwright before web-server cleanup finishes.
    exec env MISE_NO_ENV=1 mise exec -- node node_modules/@playwright/test/cli.js test "$@"

# Run a manual local REST responsiveness benchmark with default script settings.
[group('dev-tooling')]
bench-rest: build
    scripts/rest-benchmark.sh

# Run configured pre-commit checks against all tracked files.
[group('dev-tooling')]
pre-commit:
    prek run --all-files

# Serve recorded UI evidence from build/ on a fixed local port.
[group('dev-tooling')]
evidence-serve:
    go run ./internal/tools/evidencegallery

# Start an interactive agent: role [-C workdir] [--prompt-file path | prompt].
[group('agents')]
[positional-arguments]
agent role *args:
    @just _agent interactive "$@"

# Run a headless agent with the same role settings as agent.
[group('agents')]
[positional-arguments]
agent-exec role *args:
    @just _agent exec "$@"

[private]
[positional-arguments]
_agent mode role *args:
    #!/usr/bin/env bash
    set -euo pipefail

    mode="$1"
    role="$2"
    shift 2
    case "$role" in
        implement|fix) model="$MINA_DEV_IMPLEMENT_MODEL"; effort="$MINA_DEV_IMPLEMENT_EFFORT" ;;
        plan) model="$MINA_DEV_PLAN_MODEL"; effort="$MINA_DEV_PLAN_EFFORT" ;;
        fleet) model="$MINA_DEV_FLEET_MODEL"; effort="$MINA_DEV_FLEET_EFFORT" ;;
        fleet-codex) model="$MINA_DEV_FLEET_CODEX_MODEL"; effort="$MINA_DEV_FLEET_CODEX_EFFORT" ;;
        review-codex) model="$MINA_DEV_REVIEW_CODEX_MODEL"; effort="$MINA_DEV_REVIEW_CODEX_EFFORT" ;;
        review-claude) model="$MINA_DEV_REVIEW_CLAUDE_MODEL"; effort="$MINA_DEV_REVIEW_CLAUDE_EFFORT" ;;
        aggregate) model="$MINA_DEV_AGGREGATE_MODEL"; effort="$MINA_DEV_AGGREGATE_EFFORT" ;;
        validate) model="$MINA_DEV_VALIDATE_MODEL"; effort="$MINA_DEV_VALIDATE_EFFORT" ;;
        guardian) model="$MINA_DEV_GUARDIAN_MODEL"; effort="$MINA_DEV_GUARDIAN_EFFORT" ;;
        garden) model="$MINA_DEV_GARDEN_MODEL"; effort="$MINA_DEV_GARDEN_EFFORT" ;;
        *) echo "unknown agent role: $role" >&2; exit 2 ;;
    esac
    workdir=.
    prompt=""
    prompt_file=""
    prompt_set=false
    while [ "$#" -gt 0 ]; do
        case "$1" in
            -C|--prompt-file|--model|--effort)
                [ "$#" -ge 2 ] || { echo "$1 requires a value" >&2; exit 2; }
                case "$1" in
                    -C) workdir="$2" ;;
                    --prompt-file) prompt_file="$2" ;;
                    # Picker overrides affect only this launch, not inherited role defaults.
                    --model) model="$2" ;;
                    --effort) effort="$2" ;;
                esac
                shift 2
                ;;
            --)
                shift
                [ "$#" -eq 1 ] && [ "$prompt_set" = false ] || { echo "expected one prompt after --" >&2; exit 2; }
                prompt="$1"; prompt_set=true; shift
                ;;
            -*) echo "unknown agent option: $1" >&2; exit 2 ;;
            *)
                [ "$prompt_set" = false ] || { echo "provide only one prompt; use -C for the working directory" >&2; exit 2; }
                prompt="$1"; prompt_set=true; shift
                ;;
        esac
    done
    cd -- "$workdir"
    if [ -n "$prompt_file" ]; then
        [ "$prompt_set" = false ] || { echo "provide prompt or --prompt-file, not both" >&2; exit 2; }
        prompt="$(cat -- "$prompt_file")"
    fi
    if [ "$mode" = exec ] && [ -z "$prompt" ]; then
        echo "headless agents require a prompt or --prompt-file" >&2
        exit 2
    fi
    case "$model" in
        gpt-*)
            args=(codex)
            if [ "$mode" = exec ]; then args+=(exec); fi
            args+=(-m "$model" -c "model_reasoning_effort=$effort")
            ;;
        claude-*)
            args=(claude)
            if [ "$mode" = exec ]; then args+=(-p --output-format text); fi
            args+=(--model "$model" --effort "$effort")
            ;;
        *) echo "unsupported model provider: $model (use gpt- or claude-)" >&2; exit 2 ;;
    esac
    if [ -n "$prompt" ]; then args+=(-- "$prompt"); fi
    printf 'agent %s: %s / %s (%s) in %s\n' "$role" "$model" "$effort" "$mode" "$PWD" >&2
    exec "${args[@]}"

# Garden eligible documentation with parallel Codex agents.
[group('agents')]
garden-docs limit="" codex=(garden_model + "/" + garden_effort):
    #!/usr/bin/env bash
    set -euo pipefail

    limit={{ quote(limit) }}
    args=(--codex {{ quote(codex) }})
    if [ -n "$limit" ]; then
        args+=(--limit "$limit")
    fi

    go run ./internal/tools/gardendocs "${args[@]}"

# Run the repository-local review loop through local Codex and Claude CLI sessions.
[group('agents')]
review-loop mode context branch_or_commit="" base_ref="" claude_model=review_claude_model codex_reviewer=(review_codex_model + "/" + review_codex_effort) codex_aggregator=(aggregate_model + "/" + aggregate_effort) codex_validator=(validate_model + "/" + validate_effort) codex_fixer=(implement_model + "/" + implement_effort) claude_effort=review_claude_effort codex_guardian=(guardian_model + "/" + guardian_effort):
    #!/usr/bin/env bash
    set -euo pipefail

    branch_or_commit={{ quote(branch_or_commit) }}
    base_ref={{ quote(base_ref) }}

    set -- \
        --claude-model {{ quote(claude_model) }} \
        --claude-effort {{ quote(claude_effort) }} \
        --codex-reviewer {{ quote(codex_reviewer) }} \
        --codex-aggregator {{ quote(codex_aggregator) }} \
        --codex-validator {{ quote(codex_validator) }} \
        --codex-fixer {{ quote(codex_fixer) }} \
        --codex-guardian {{ quote(codex_guardian) }}
    if [ -n "$base_ref" ]; then
        set -- "$@" --base "$base_ref"
    fi

    log_dir="build/review-loop"
    mkdir -p "$log_dir"
    log_file="$log_dir/review-loop-$(date -u +%Y%m%dT%H%M%SZ)-$$.log"
    echo "review loop log: $log_file"

    review_command=(go run ./internal/tools/reviewloop "$@" {{ quote(mode) }} {{ quote(context) }})
    if [ -n "$branch_or_commit" ]; then
        review_command+=("$branch_or_commit")
    fi
    "${review_command[@]}" 2>&1 | tee "$log_file"

# Tail the most recently updated review-loop log with optional tail arguments.
[group('agents')]
[positional-arguments]
review-loop-tail *tail_args:
    #!/usr/bin/env bash
    set -euo pipefail

    log_dir="build/review-loop"
    latest_log=""
    shopt -s nullglob
    for log_file in "$log_dir"/review-loop-*.log; do
        if [ -z "$latest_log" ] || [ "$log_file" -nt "$latest_log" ]; then
            latest_log="$log_file"
        fi
    done
    if [ -z "$latest_log" ]; then
        echo "no review-loop logs found in $log_dir" >&2
        exit 1
    fi

    echo "tailing review loop log: $latest_log"
    tail "$@" "$latest_log"

# Run Codex against an implementation plan.
[group('agents')]
codex-goal plan_file="":
    #!/usr/bin/env bash
    set -euo pipefail

    plan_file={{ quote(plan_file) }}
    if [ -z "$plan_file" ]; then
        command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; }
        if ! plan_file="$(
            find docs/plans -maxdepth 1 -type f -name '*.md' | sort | fzf \
                --prompt='Plan> ' \
                --preview='sed -n "1,120p" {}'
        )"; then
            echo "no plan selected" >&2
            exit 1
        fi
        [ -n "$plan_file" ] || { echo "no plan selected" >&2; exit 1; }
    fi

    exec just agent implement "/goal Implement @${plan_file}. The plan's completion criteria define when the goal is complete."

# Run a Codex operator against a sequential fleet plan.
[group('agents')]
codex-goal-fleet plan_file="":
    #!/usr/bin/env bash
    set -euo pipefail

    plan_file={{ quote(plan_file) }}
    command -v codex >/dev/null || { echo "missing required tool: codex" >&2; exit 1; }
    if [ -z "$plan_file" ]; then
        command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; }
        if ! plan_file="$(
            find docs/plans -maxdepth 1 -type f -name '*-fleet.md' | \
                while IFS= read -r candidate; do
                    if [[ "$candidate" =~ ^docs/plans/[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(-[a-z0-9]+)*-fleet\.md$ ]]; then
                        printf '%s\n' "$candidate"
                    fi
                done | sort | fzf \
                --prompt='Fleet plan> ' \
                --preview='sed -n "1,160p" {}'
        )"; then
            echo "no fleet plan selected" >&2
            exit 1
        fi
        [ -n "$plan_file" ] || { echo "no fleet plan selected" >&2; exit 1; }
    fi

    [[ "$plan_file" =~ ^docs/plans/[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(-[a-z0-9]+)*-fleet\.md$ ]] || {
        echo "fleet plan must match docs/plans/YYYY-MM-DD-<topic>-fleet.md" >&2
        exit 1
    }
    [ -f "$plan_file" ] || { echo "fleet plan not found: $plan_file" >&2; exit 1; }

    exec just agent fleet-codex "/goal Operate @${plan_file}. The plan's completion criteria define when the goal is complete."

# List currently actionable, unclaimed parent Kata issues as tab-separated rows.
[private]
kata-open-issues:
    #!/usr/bin/env bash
    set -euo pipefail

    command -v jq >/dev/null || { echo "missing required tool: jq" >&2; exit 1; }
    command -v kata >/dev/null || { echo "missing required tool: kata" >&2; exit 1; }

    jq -r -s '
        (.[1].issues
            | map(select(.parent != null) | .parent.short_id)
            | unique) as $parents
        | .[0].issues
        | map(select((.short_id as $id | $parents | index($id) | not)))
        | group_by(.priority // 5)[]
        | sort_by(.updated_at)
        | reverse[]
        | [
            .short_id,
            ("P" + (.priority | tostring)),
            .updated_at,
            .title
        ]
        | @tsv
    ' <(kata ready --unowned --json --limit 0) <(kata list --status all --limit 1000 --json)

# Start a kata plan-only worktree through Codex.
[group('agents')]
codex-kata-plan:
    #!/usr/bin/env bash
    set -euo pipefail

    command -v codex >/dev/null || { echo "missing required tool: codex" >&2; exit 1; }
    command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; }

    if ! selected="$(
        just --quiet kata-open-issues | fzf \
            --prompt='Kata> ' \
            --delimiter=$'\t' \
            --with-nth=1,2,4 \
            --preview='kata show {1} --agent' \
            --preview-window='right:50%:wrap-word'
    )"; then
        echo "no kata issue selected" >&2
        exit 1
    fi
    [ -n "$selected" ] || { echo "no kata issue selected" >&2; exit 1; }

    issue="${selected%%$'\t'*}"
    exec just agent-exec plan "\$kata-plan-worktree #${issue}"

# Create a worktree for a selected Kata issue and launch an implementation agent.
[group('agents')]
kata-implement:
    #!/usr/bin/env bash
    set -euo pipefail

    command -v fzf >/dev/null || { echo "missing required tool: fzf" >&2; exit 1; }
    command -v git >/dev/null || { echo "missing required tool: git" >&2; exit 1; }
    command -v gt >/dev/null || { echo "missing required tool: gt" >&2; exit 1; }
    command -v jq >/dev/null || { echo "missing required tool: jq" >&2; exit 1; }
    command -v kata >/dev/null || { echo "missing required tool: kata" >&2; exit 1; }

    if ! selected="$(
        just --quiet kata-open-issues | fzf \
            --prompt='Kata> ' \
            --delimiter=$'\t' \
            --with-nth=1,2,4 \
            --preview='kata show {1} --agent' \
            --preview-window='right:50%:wrap-word'
    )"; then
        echo "no kata issue selected" >&2
        exit 1
    fi
    [ -n "$selected" ] || { echo "no kata issue selected" >&2; exit 1; }
    issue="${selected%%$'\t'*}"

    if ! agent_model_thinking="$(
        printf '%s\n' {{ quote(agent_model_thinking_levels) }} | fzf --prompt='Agent model / thinking> '
    )"; then
        echo "no agent model and thinking level selected" >&2
        exit 1
    fi
    [ -n "$agent_model_thinking" ] || { echo "no agent model and thinking level selected" >&2; exit 1; }
    case "${agent_model_thinking%/*}" in
        gpt-*) agent_cli=codex ;;
        claude-*) agent_cli=claude ;;
        *) echo "unsupported agent model: ${agent_model_thinking%/*}" >&2; exit 2 ;;
    esac
    command -v "$agent_cli" >/dev/null || { echo "missing required tool: $agent_cli" >&2; exit 1; }

    kata claim "$issue" --agent

    issue_json="$(kata show "$issue" --json)"
    title="$(jq -r '.issue.title' <<<"$issue_json")"
    issue_body="$(jq -r '.issue.body // ""' <<<"$issue_json")"
    slug="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
    [ -n "$slug" ] || { echo "could not create a branch slug from Kata issue title" >&2; exit 1; }
    branch="${issue}-${slug}"

    git fetch origin main
    gt "$branch" main -x true
    worktree_path="$(
        git worktree list --porcelain | awk -v branch="refs/heads/$branch" '
            $1 == "worktree" { path = $2 }
            $1 == "branch" && $2 == branch { print path; exit }
        '
    )"
    [ -n "$worktree_path" ] || { echo "could not find worktree for branch $branch" >&2; exit 1; }

    prompt_template="$(<docs/kata-implementation-prompt-template.md)"
    prompt="${prompt_template//\{\{issue\}\}/#$issue}"
    prompt="${prompt//\{\{issue_body\}\}/$issue_body}"
    exec just agent implement -C "$worktree_path" --model "${agent_model_thinking%/*}" --effort "${agent_model_thinking##*/}" -- "$prompt"

# Rebase the current branch through Codex.
[group('agents')]
rebase:
    just agent-exec implement {{ quote("$rebase") }}

# Agent-only manual smoke commands should be added temporarily when a concrete uncovered risk remains outside the testscript end-to-end suite.