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

View File

@@ -1,3 +1,58 @@
# orchestrator # agent-coding-empire orchestrator
Glue between Gitea and the dev/auditor LXCs. PM tools, webhook handler, dev-task wrapper, auditor workflow. Glue between Gitea and the dev/auditor LXCs that run `claude-code` headless.
## Architecture (v0)
```
┌──────────────────────────────┐
│ Gitea (CT 101) │
│ - Issues with labels │
│ - PRs │
│ - Actions runner (CT 112) │
└────────┬─────────────────────┘
│ webhook
┌──────────────────────────────────────┐
│ orchestrator (CT 103, this repo) │
│ - webhook handler (Phase 2) │
│ - PM helper scripts │
│ - SSH-triggers dev-task on dev LXCs │
└────────┬─────────────────────────────┘
│ SSH
┌──────────────────┐
│ dev-01 (CT 120) │ one slot active at a time
│ - claude-code │
│ - tea CLI │
│ - dev-task.sh │
└──────────────────┘
```
## Constraints
- **Max-plan auth shared.** Each dev LXC has a copy of `~/.claude/.credentials.json`.
They all draw from the same rate-limit pool as Danny's interactive sessions.
- **One dev agent active at a time** in v0 (no parallel) to avoid starving the pool.
- **Default model = sonnet.** Opus only when issue body explicitly says `Model: opus`.
- **Hard wallclock cap** = 30 minutes per task (configurable via `MAX_WALLCLOCK`).
## Repo layout
```
agents/
dev-task.sh # runs inside dev LXC; takes <repo> <issue-id>, opens PR
pm/
(todo) # PM scripts that create structured issues
auditor/
(todo) # Gitea Actions workflow for PR review
provisioning/
(todo) # scripts to bootstrap a fresh dev LXC
```
## Phase status
- [x] Phase 0: Gitea repos, dev-01 LXC, claude-code + tea installed, auth verified
- [ ] Phase 1: manual closed loop (PM writes issue → dev-task → PR)
- [ ] Phase 2: webhook-driven automation
- [ ] Phase 3: multi-dev (still one active at a time)

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"

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# bootstrap-dev-lxc.sh — provision a fresh dev LXC.
# Run from the PVE host as root. Idempotent.
#
# Usage:
# ./bootstrap-dev-lxc.sh <CTID> [gitea_token_file]
#
# Assumes:
# - CT already exists and is started
# - You have local copies of claude-code (in /tmp/claude-staging) and tea
# (in /tmp/tea-staging) and credentials (/tmp/claude-creds-staging.json)
#
# This is intentionally not a full image builder — it's the manual recipe
# that documents what dev-01 was bootstrapped with. Codify later.
set -euo pipefail
CTID="${1:?usage: $0 <CTID> [gitea_token_file]}"
TOKEN_FILE="${2:-/tmp/gitea-token-staging}"
[[ -r /tmp/claude-staging ]] || { echo "missing /tmp/claude-staging (claude-code tree)" >&2; exit 1; }
[[ -r /tmp/tea-staging ]] || { echo "missing /tmp/tea-staging (tea binary)" >&2; exit 1; }
[[ -r /tmp/claude-creds-staging.json ]] || { echo "missing /tmp/claude-creds-staging.json" >&2; exit 1; }
[[ -r "$TOKEN_FILE" ]] || { echo "missing token file $TOKEN_FILE" >&2; exit 1; }
# --- packages ---
pct exec "$CTID" -- bash -c '
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
git curl ca-certificates jq openssh-server xz-utils
mkdir -p /root/.claude /root/.local/bin /root/.local/share /root/.ssh /etc/agent /var/agent/workspaces /var/agent/logs
chmod 700 /root/.ssh /root/.claude /etc/agent
'
# --- claude-code ---
tar -czf /tmp/claude-bundle.tgz -C /tmp claude-staging
pct push "$CTID" /tmp/claude-bundle.tgz /tmp/claude-bundle.tgz
pct push "$CTID" /tmp/claude-creds-staging.json /root/.claude/.credentials.json --perms 600
pct exec "$CTID" -- bash -c '
set -e
rm -rf /root/.local/share/claude
tar -xzf /tmp/claude-bundle.tgz -C /root/.local/share/
mv /root/.local/share/claude-staging /root/.local/share/claude
CLAUDE_VERSION=$(ls /root/.local/share/claude/versions/ | sort -V | tail -1)
ln -sf /root/.local/share/claude/versions/$CLAUDE_VERSION /root/.local/bin/claude
rm -f /tmp/claude-bundle.tgz
'
rm -f /tmp/claude-bundle.tgz
# --- tea ---
pct push "$CTID" /tmp/tea-staging /usr/local/bin/tea --perms 755
# --- gitea token ---
pct push "$CTID" "$TOKEN_FILE" /etc/agent/gitea-token --perms 600
# --- enable sshd ---
pct exec "$CTID" -- systemctl enable --now ssh
echo "Bootstrap complete for CT $CTID. Versions:"
pct exec "$CTID" -- bash -lc 'PATH=/root/.local/bin:/usr/local/bin:$PATH; claude --version; tea --version | head -2'