#!/usr/bin/env bash # dev-task.sh — invoked inside a dev LXC. Picks up a Gitea issue, runs # claude-code headless to implement it, and opens a PR back to the repo. # # Usage: dev-task.sh / # Env knobs (all optional): # MAX_WALLCLOCK default 30m — coreutils-timeout duration string # MODEL default sonnet — overridden by "Model: opus" line in issue body # BASE_BRANCH default main # GITEA_URL default https://gitea.dannyhaslund.dk # GITEA_TOKEN_FILE default /etc/agent/gitea-token # WORK_ROOT default /var/agent/workspaces # LOG_ROOT default /var/agent/logs set -euo pipefail REPO="${1:-}" ISSUE="${2:-}" [[ -z "$REPO" || -z "$ISSUE" ]] && { echo "usage: $0 / " >&2; exit 64; } MAX_WALLCLOCK="${MAX_WALLCLOCK:-30m}" MODEL_DEFAULT="${MODEL:-sonnet}" BASE_BRANCH="${BASE_BRANCH:-main}" GITEA_URL="${GITEA_URL:-https://gitea.dannyhaslund.dk}" GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-/etc/agent/gitea-token}" WORK_ROOT="${WORK_ROOT:-/var/agent/workspaces}" LOG_ROOT="${LOG_ROOT:-/var/agent/logs}" [[ -r "$GITEA_TOKEN_FILE" ]] || { echo "missing $GITEA_TOKEN_FILE" >&2; exit 65; } GITEA_TOKEN="$(cat "$GITEA_TOKEN_FILE")" mkdir -p "$WORK_ROOT" "$LOG_ROOT" LOG_FILE="$LOG_ROOT/issue-${ISSUE}-$(date +%Y%m%dT%H%M%S).log" log() { printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*" | tee -a "$LOG_FILE"; } # ---------- 1. Fetch issue ---------- log "Fetching issue $REPO#$ISSUE" issue_json="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_URL/api/v1/repos/$REPO/issues/$ISSUE")" title="$(jq -r .title <<<"$issue_json")" body="$(jq -r .body <<<"$issue_json")" # ---------- 2. Pick model from body if specified ---------- if echo "$body" | grep -qiE '^\s*model:\s*opus' ; then MODEL="opus" else MODEL="$MODEL_DEFAULT" fi log "Using model: $MODEL (default=$MODEL_DEFAULT)" # ---------- 3. Workspace + branch ---------- slug="$(echo "$title" | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' \ | cut -c1-50)" [[ -z "$slug" ]] && slug="task" BRANCH="agent/issue-${ISSUE}-${slug}" WORKDIR="$WORK_ROOT/${REPO//\//_}-issue-${ISSUE}" rm -rf "$WORKDIR" log "Cloning into $WORKDIR" git clone --quiet \ "https://oauth2:${GITEA_TOKEN}@${GITEA_URL#https://}/${REPO}.git" \ "$WORKDIR" cd "$WORKDIR" git config user.email "agent-dev01@dannyhaslund.dk" git config user.name "agent dev-01" git checkout -q "$BASE_BRANCH" git checkout -q -b "$BRANCH" # ---------- 4. Build prompt ---------- PROMPT_FILE="$(mktemp)" cat > "$PROMPT_FILE" <>"$LOG_FILE" 2>&1 claude_rc=$? set -e rm -f "$PROMPT_FILE" log "claude exited with rc=$claude_rc" if [[ $claude_rc -eq 124 || $claude_rc -eq 137 ]]; then log "TIMEOUT — claude was killed at wallclock $MAX_WALLCLOCK" elif [[ $claude_rc -ne 0 ]]; then log "claude exited non-zero (see $LOG_FILE)" fi # ---------- 6. Inspect git state ---------- if [[ -n "$(git status --porcelain)" ]]; then log "Uncommitted changes found — committing as WIP" git add -A git commit -q -m "WIP: leftover changes from claude session" fi commits_ahead="$(git rev-list --count "$BASE_BRANCH..HEAD")" log "Commits ahead of $BASE_BRANCH: $commits_ahead" if [[ "$commits_ahead" -eq 0 ]]; then log "No commits produced. Posting comment on issue and exiting." body_json="$(jq -nc --arg b "agent dev-01: no commits produced for issue #$ISSUE. Log: \`$LOG_FILE\`. claude rc=$claude_rc" '{body:$b}')" curl -fsS -X POST \ -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ -d "$body_json" \ "$GITEA_URL/api/v1/repos/$REPO/issues/$ISSUE/comments" >/dev/null exit 2 fi # ---------- 7. Push branch ---------- log "Pushing branch $BRANCH" git push --quiet -u origin "$BRANCH" # ---------- 8. Open PR via Gitea API ---------- pr_body="$(printf 'Resolves #%s\n\nAutomated PR from agent dev-01.\n\n- model: `%s`\n- log: `%s`\n- claude rc: `%s`\n' "$ISSUE" "$MODEL" "$LOG_FILE" "$claude_rc")" pr_json="$(jq -nc \ --arg title "$title" \ --arg body "$pr_body" \ --arg head "$BRANCH" \ --arg base "$BASE_BRANCH" \ '{title:$title, body:$body, head:$head, base:$base}')" pr_resp="$(curl -fsS -X POST \ -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ -d "$pr_json" \ "$GITEA_URL/api/v1/repos/$REPO/pulls")" pr_number="$(jq -r .number <<<"$pr_resp")" pr_url="$(jq -r .html_url <<<"$pr_resp")" log "PR opened: #$pr_number → $pr_url" # ---------- 9. Comment on the issue ---------- comment_body="$(jq -nc --arg b "agent dev-01: PR opened → $pr_url" '{body:$b}')" curl -fsS -X POST \ -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ -d "$comment_body" \ "$GITEA_URL/api/v1/repos/$REPO/issues/$ISSUE/comments" >/dev/null log "Done." echo "$pr_url"