#!/usr/bin/env bash
# Purpose: run one coding task through one agent, from an identical starting state every
#          time, and record wall-clock, outcome, tokens and the size of the change as one
#          JSON line in the lab notebook.
# Platform: all (spark, strix, mac, nvidia). Track M runs it natively or inside the
#          sandbox container from this part's second lab.
# Minimum memory: 16 GB for a 30B-class coder behind the gateway alias
# Assumes: the Part 9 gateway is running and MODEL names one of its aliases; the tool being
#          tested is installed and already configured against that gateway (each tool's
#          lesson in this part shows how); task-app.py, task-tests.py and task-readme.md
#          are in $TASK_SRC; and python3 with pytest is available.
#          Flags used for each tool are the ones documented on that tool's pages as read on
#          2026-09-09. Confirm them against your installed version with --help before the
#          first run; a flag that has been renamed fails inside an unattended run.
#
# Usage:
#   ./run-agent-task.sh aider
#   TOKENS=48210 ./run-agent-task.sh codex
#   USAGE_LOG=~/gateway/usage.jsonl ./run-agent-task.sh opencode

set -euo pipefail

TOOL="${1:-}"
if [ -z "$TOOL" ]; then
    echo "usage: $0 <aider|codex|opencode|claude|goose|openhands|manual>" >&2
    exit 2
fi

TASK_SRC="${TASK_SRC:-$PWD/task}"
WORK="${WORK:-$PWD/run-$TOOL}"
LABBOOK="${LABBOOK:-$PWD/labbook.md}"
MODEL="${MODEL:-local/coder}"
TEST_CMD="${TEST_CMD:-python3 -m pytest -q task-tests.py}"
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-1800}"
# Tokens for this run. Read it from the gateway's usage records for the virtual key you
# gave this tool, and pass it in; the script fills it in itself only when USAGE_LOG points
# at a JSON-lines file the gateway appends to.
TOKENS="${TOKENS:-}"
USAGE_LOG="${USAGE_LOG:-}"

PROMPT="Read task-readme.md, then make every test in task-tests.py pass by changing only \
task-app.py. Do not edit task-tests.py. Run the tests before and after each change. When \
the suite passes, state in one sentence what was wrong."

for required in python3 git; do
    if ! command -v "$required" >/dev/null 2>&1; then
        echo "$required is required and was not found on PATH." >&2
        exit 1
    fi
done

if [ ! -d "$TASK_SRC" ]; then
    echo "Task directory not found: $TASK_SRC" >&2
    echo "It must contain task-app.py, task-tests.py and task-readme.md." >&2
    exit 1
fi

# ------------------------------------------------------------------ a clean start, always
# Every tool gets a byte-identical starting state. Without this the second tool inherits
# the first one's edits and the comparison measures nothing.
rm -rf "$WORK"
mkdir -p "$WORK"
cp "$TASK_SRC"/task-app.py "$TASK_SRC"/task-tests.py "$TASK_SRC"/task-readme.md "$WORK"/
cp "$WORK"/task-app.py "$WORK"/.task-app-original.py

usage_bytes() {
    if [ -n "$USAGE_LOG" ] && [ -f "$USAGE_LOG" ]; then
        wc -c <"$USAGE_LOG" | tr -d ' '
    else
        echo 0
    fi
}

USAGE_BEFORE="$(usage_bytes)"

# ------------------------------------------------------------------------- the tool itself
run_tool() {
    case "$TOOL" in
        aider)
            aider --model "openai/$MODEL" \
                --message "$PROMPT" \
                --test-cmd "$TEST_CMD" \
                --auto-test \
                --no-auto-commits \
                --yes-always \
                task-app.py
            ;;
        codex)
            codex exec \
                --sandbox workspace-write \
                --ask-for-approval never \
                "$PROMPT"
            ;;
        opencode)
            opencode run --model "gateway/$MODEL" "$PROMPT"
            ;;
        claude)
            claude --print --permission-mode acceptEdits "$PROMPT"
            ;;
        goose)
            goose run --recipe "$TASK_SRC/goose-recipe.yaml" \
                --params "project_dir=$WORK" \
                --no-session \
                --max-turns 40
            ;;
        openhands)
            openhands --headless -t "$PROMPT"
            ;;
        manual)
            echo "Do the task by hand in your editor agent, in $WORK, then press Enter."
            read -r _
            ;;
        *)
            echo "Unknown tool: $TOOL" >&2
            exit 2
            ;;
    esac
}

START="$(date +%s)"
TOOL_STATUS=0
(
    cd "$WORK"
    run_tool
) >"$WORK/transcript.txt" 2>&1 || TOOL_STATUS=$?
END="$(date +%s)"
ELAPSED=$(( END - START ))

if [ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ]; then
    echo "Note: the run took at least the configured timeout of ${TIMEOUT_SECONDS}s." >&2
fi

# ---------------------------------------------------------------------------- the verdict
TESTS_STATUS=0
(
    cd "$WORK"
    eval "$TEST_CMD"
) >"$WORK/test-output.txt" 2>&1 || TESTS_STATUS=$?

TESTS_EDITED=false
if ! diff -q "$TASK_SRC/task-tests.py" "$WORK/task-tests.py" >/dev/null 2>&1; then
    TESTS_EDITED=true
fi

CHANGED_LINES="$(diff -u "$WORK/.task-app-original.py" "$WORK/task-app.py" \
    | grep -c -E '^[+-][^+-]' || true)"

USAGE_AFTER="$(usage_bytes)"
if [ -z "$TOKENS" ] && [ -n "$USAGE_LOG" ] && [ "$USAGE_AFTER" -gt "$USAGE_BEFORE" ]; then
    TOKENS="$(USAGE_LOG="$USAGE_LOG" OFFSET="$USAGE_BEFORE" python3 - <<'PY'
import json
import os

total = 0
with open(os.environ["USAGE_LOG"], "r", encoding="utf-8", errors="replace") as handle:
    handle.seek(int(os.environ["OFFSET"]))
    for line in handle:
        line = line.strip()
        if not line:
            continue
        try:
            record = json.loads(line)
        except json.JSONDecodeError:
            continue
        total += int(record.get("prompt_tokens", 0)) + int(record.get("completion_tokens", 0))
print(total)
PY
)"
fi

# ------------------------------------------------------------------- one line, one result
TOOL="$TOOL" MODEL="$MODEL" ELAPSED="$ELAPSED" TOOL_STATUS="$TOOL_STATUS" \
TESTS_STATUS="$TESTS_STATUS" TESTS_EDITED="$TESTS_EDITED" \
CHANGED_LINES="$CHANGED_LINES" TOKENS="${TOKENS:-}" LABBOOK="$LABBOOK" \
python3 - <<'PY'
import datetime
import json
import os

tokens = os.environ.get("TOKENS") or ""
record = {
    "lab": "part-25-one-task-six-agents",
    "recorded": datetime.datetime.now(datetime.timezone.utc)
    .replace(microsecond=0)
    .isoformat(),
    "tool": os.environ["TOOL"],
    "model_alias": os.environ["MODEL"],
    "wall_clock_seconds": int(os.environ["ELAPSED"]),
    "tool_exit_status": int(os.environ["TOOL_STATUS"]),
    "tests_pass": os.environ["TESTS_STATUS"] == "0",
    "tests_were_edited": os.environ["TESTS_EDITED"] == "true",
    "changed_lines": int(os.environ["CHANGED_LINES"] or 0),
    "tokens": int(tokens) if tokens.isdigit() else None,
}
with open(os.environ["LABBOOK"], "a", encoding="utf-8") as handle:
    handle.write(json.dumps(record) + "\n")
print(json.dumps(record, indent=2))
PY

echo
echo "transcript:  $WORK/transcript.txt"
echo "test output: $WORK/test-output.txt"
echo "recorded in: $LABBOOK"

if [ "$TESTS_EDITED" = "true" ]; then
    echo
    echo "WARNING: task-tests.py was modified. This run does not count as a pass;" >&2
    echo "record it as a failure and note what the agent changed." >&2
fi
