Phase 0: dev-task wrapper + LXC bootstrap recipe + arch README
This commit is contained in:
59
README.md
59
README.md
@@ -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
165
agents/dev-task.sh
Executable 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"
|
||||||
59
provisioning/bootstrap-dev-lxc.sh
Executable file
59
provisioning/bootstrap-dev-lxc.sh
Executable 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'
|
||||||
Reference in New Issue
Block a user