Phase 0: dev-task wrapper + LXC bootstrap recipe + arch README

This commit is contained in:
danny8632
2026-05-12 06:41:18 +00:00
parent 5e76d18338
commit e34a9930c5
3 changed files with 281 additions and 2 deletions

165
agents/dev-task.sh Executable file
View File

@@ -0,0 +1,165 @@
#!/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 <owner>/<repo> <issue-id>
# 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 <owner>/<repo> <issue-id>" >&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" <<EOF
You are an autonomous developer agent. Implement the following Gitea issue in
the current working directory. The repository \`$REPO\` is already cloned and
checked out on branch \`$BRANCH\` (based on \`$BASE_BRANCH\`).
# Issue #$ISSUE — $title
$body
# Working constraints
- Make atomic git commits with clear messages. Use \`git\` directly via Bash.
- Do NOT open the pull request yourself — this wrapper opens the PR after you
finish, based on the commits on this branch.
- If the issue is impossible or under-specified, leave the branch in whatever
partial state best documents your investigation, summarize what is missing
in your final response, and stop.
- Do not push the branch — the wrapper pushes after you exit.
- Stop as soon as the Done criteria in the issue are satisfied. Do not
refactor unrelated code, add features beyond the issue, or restructure the
repo.
EOF
# ---------- 5. Invoke claude-code ----------
log "Starting claude (model=$MODEL, timeout=$MAX_WALLCLOCK)"
set +e
timeout --signal=INT --kill-after=30s "$MAX_WALLCLOCK" \
/root/.local/bin/claude \
-p "$(cat "$PROMPT_FILE")" \
--model "$MODEL" \
--permission-mode bypassPermissions \
--output-format text \
--add-dir "$WORKDIR" \
>>"$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"