refactor: make all scripts multi-project via env vars

Replace hardcoded harb references across the entire codebase:
- HARB_REPO_ROOT → PROJECT_REPO_ROOT (with deprecated alias)
- Derive PROJECT_NAME from CODEBERG_REPO slug
- Add PRIMARY_BRANCH (master/main), WOODPECKER_REPO_ID env vars
- Parameterize worktree prefixes, docker container names, branch refs
- Genericize agent prompts (gardener, factory supervisor)
- Update best-practices docs to use $-vars, prefix harb lessons

All project-specific values now flow from .env → lib/env.sh → scripts.
Backward-compatible: existing harb setups work without .env changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
johba 2026-03-14 13:49:09 +01:00
parent f16df6c53e
commit 90ef03a304
16 changed files with 117 additions and 116 deletions

View file

@ -2,17 +2,23 @@
# Copy to .env and fill in your values. # Copy to .env and fill in your values.
# NEVER commit .env to the repo. # NEVER commit .env to the repo.
# ── Target project ────────────────────────────────────────────────────────
CODEBERG_REPO=johba/yourproject # org/repo slug on Codeberg
PROJECT_REPO_ROOT=/home/you/yourproject # local clone of the target repo
PRIMARY_BRANCH=main # main or master
# PROJECT_NAME=yourproject # optional — auto-derived from CODEBERG_REPO
# ── Auth tokens ───────────────────────────────────────────────────────────
# Codeberg API token (read from ~/.netrc by default, override here if needed) # Codeberg API token (read from ~/.netrc by default, override here if needed)
# CODEBERG_TOKEN= # CODEBERG_TOKEN=
# Codeberg review bot token (separate account for formal reviews) # Codeberg review bot token (separate account for formal reviews)
REVIEW_BOT_TOKEN= REVIEW_BOT_TOKEN=
# Woodpecker CI API token # ── Woodpecker CI ─────────────────────────────────────────────────────────
WOODPECKER_TOKEN= WOODPECKER_TOKEN=
# Woodpecker CI server URL
WOODPECKER_SERVER=http://localhost:8000 WOODPECKER_SERVER=http://localhost:8000
WOODPECKER_REPO_ID=2 # numeric repo ID in Woodpecker DB
# Woodpecker Postgres (for direct DB queries) # Woodpecker Postgres (for direct DB queries)
WOODPECKER_DB_PASSWORD= WOODPECKER_DB_PASSWORD=
@ -20,12 +26,5 @@ WOODPECKER_DB_USER=woodpecker
WOODPECKER_DB_HOST=127.0.0.1 WOODPECKER_DB_HOST=127.0.0.1
WOODPECKER_DB_NAME=woodpecker WOODPECKER_DB_NAME=woodpecker
# Target Codeberg repo # ── Tuning ────────────────────────────────────────────────────────────────
CODEBERG_REPO=johba/harb CLAUDE_TIMEOUT=7200 # max seconds per Claude invocation
CODEBERG_API=https://codeberg.org/api/v1/repos/johba/harb
# Harb repo local path
HARB_REPO_ROOT=/home/debian/harb
# Claude CLI timeout (seconds)
CLAUDE_TIMEOUT=7200

View file

@ -20,7 +20,7 @@
# {"status": "already_done", "reason": "..."} # {"status": "already_done", "reason": "..."}
# #
# Peek: cat /tmp/dev-agent-status # Peek: cat /tmp/dev-agent-status
# Log: tail -f ~/scripts/harb-dev/dev-agent.log # Log: tail -f dev-agent.log
set -euo pipefail set -euo pipefail
@ -31,7 +31,7 @@ source "$(dirname "$0")/../lib/env.sh"
# --- Config --- # --- Config ---
ISSUE="${1:?Usage: dev-agent.sh <issue-number>}" ISSUE="${1:?Usage: dev-agent.sh <issue-number>}"
REPO="${CODEBERG_REPO}" REPO="${CODEBERG_REPO}"
REPO_ROOT="${HARB_REPO_ROOT}" REPO_ROOT="${PROJECT_REPO_ROOT}"
API="${CODEBERG_API}" API="${CODEBERG_API}"
LOCKFILE="/tmp/dev-agent.lock" LOCKFILE="/tmp/dev-agent.lock"
@ -39,7 +39,7 @@ STATUSFILE="/tmp/dev-agent-status"
LOGFILE="${FACTORY_ROOT}/dev/dev-agent.log" LOGFILE="${FACTORY_ROOT}/dev/dev-agent.log"
PREFLIGHT_RESULT="/tmp/dev-agent-preflight.json" PREFLIGHT_RESULT="/tmp/dev-agent-preflight.json"
BRANCH="fix/issue-${ISSUE}" BRANCH="fix/issue-${ISSUE}"
WORKTREE="/tmp/harb-worktree-${ISSUE}" WORKTREE="/tmp/${PROJECT_NAME}-worktree-${ISSUE}"
REVIEW_POLL_INTERVAL=300 # 5 min between review checks REVIEW_POLL_INTERVAL=300 # 5 min between review checks
MAX_REVIEW_ROUNDS=5 MAX_REVIEW_ROUNDS=5
CLAUDE_TIMEOUT=7200 CLAUDE_TIMEOUT=7200
@ -101,7 +101,7 @@ write_state_entry() {
description=$(echo "$ISSUE_TITLE" | sed 's/^feat:\s*//i;s/^fix:\s*//i;s/^refactor:\s*//i') description=$(echo "$ISSUE_TITLE" | sed 's/^feat:\s*//i;s/^fix:\s*//i;s/^refactor:\s*//i')
local line="- [${today}] ${description} (#${ISSUE})" local line="- [${today}] ${description} (#${ISSUE})"
if [ ! -f "$state_file" ]; then if [ ! -f "$state_file" ]; then
printf '# STATE.md — What harb currently is and does\n\n' > "$state_file" printf '# STATE.md — What %s currently is and does\n\n' "${PROJECT_NAME}" > "$state_file"
fi fi
echo "$line" >> "$state_file" echo "$line" >> "$state_file"
log "STATE.md: ${line}" log "STATE.md: ${line}"
@ -425,7 +425,7 @@ if [ -n "$EXISTING_PR" ]; then
fi fi
REVIEW_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}. REVIEW_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}.
This is issue #${ISSUE} for the harb DeFi protocol. This is issue #${ISSUE} for the ${CODEBERG_REPO} project.
## Issue: ${ISSUE_TITLE} ## Issue: ${ISSUE_TITLE}
@ -539,28 +539,28 @@ else
status "creating worktree" status "creating worktree"
cd "$REPO_ROOT" cd "$REPO_ROOT"
# Ensure repo is in clean state (abort stale rebases, checkout master) # Ensure repo is in clean state (abort stale rebases, checkout primary branch)
if [ -d "$REPO_ROOT/.git/rebase-merge" ] || [ -d "$REPO_ROOT/.git/rebase-apply" ]; then if [ -d "$REPO_ROOT/.git/rebase-merge" ] || [ -d "$REPO_ROOT/.git/rebase-apply" ]; then
log "WARNING: stale rebase detected in main repo — aborting" log "WARNING: stale rebase detected in main repo — aborting"
git rebase --abort 2>/dev/null || true git rebase --abort 2>/dev/null || true
fi fi
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
if [ "$CURRENT_BRANCH" != "master" ]; then if [ "$CURRENT_BRANCH" != "${PRIMARY_BRANCH}" ]; then
log "WARNING: main repo on '$CURRENT_BRANCH' instead of master — switching" log "WARNING: main repo on '$CURRENT_BRANCH' instead of ${PRIMARY_BRANCH} — switching"
git checkout master 2>/dev/null || true git checkout "${PRIMARY_BRANCH}" 2>/dev/null || true
fi fi
git fetch origin master 2>/dev/null git fetch origin "${PRIMARY_BRANCH}" 2>/dev/null
git pull --ff-only origin master 2>/dev/null || true git pull --ff-only origin "${PRIMARY_BRANCH}" 2>/dev/null || true
cleanup_worktree cleanup_worktree
git worktree add "$WORKTREE" origin/master -B "$BRANCH" 2>&1 || { git worktree add "$WORKTREE" "origin/${PRIMARY_BRANCH}" -B "$BRANCH" 2>&1 || {
log "ERROR: worktree creation failed" log "ERROR: worktree creation failed"
git worktree add "$WORKTREE" origin/master -B "$BRANCH" 2>&1 | while read -r wt_line; do log " $wt_line"; done || true git worktree add "$WORKTREE" "origin/${PRIMARY_BRANCH}" -B "$BRANCH" 2>&1 | while read -r wt_line; do log " $wt_line"; done || true
cleanup_labels cleanup_labels
exit 1 exit 1
} }
cd "$WORKTREE" cd "$WORKTREE"
git checkout -B "$BRANCH" origin/master 2>/dev/null git checkout -B "$BRANCH" "origin/${PRIMARY_BRANCH}" 2>/dev/null
git submodule update --init --recursive 2>/dev/null || true git submodule update --init --recursive 2>/dev/null || true
# Write STATE.md entry — included in the first commit, reads as done once PR merges # Write STATE.md entry — included in the first commit, reads as done once PR merges
@ -581,7 +581,7 @@ else
jq -r '.[] | "#\(.number) \(.title)"' 2>/dev/null || echo "(could not fetch)") jq -r '.[] | "#\(.number) \(.title)"' 2>/dev/null || echo "(could not fetch)")
PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}. PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}.
You have been assigned issue #${ISSUE} for the harb DeFi protocol. You have been assigned issue #${ISSUE} for the ${CODEBERG_REPO} project.
## Issue: ${ISSUE_TITLE} ## Issue: ${ISSUE_TITLE}
@ -680,7 +680,7 @@ If you cannot or should not implement this issue, output ONLY a JSON object (no
# But only treat as refusal if there are NO commits (claude might output JSON-like text AND commit) # But only treat as refusal if there are NO commits (claude might output JSON-like text AND commit)
cd "$WORKTREE" cd "$WORKTREE"
AHEAD=$(git rev-list origin/master..HEAD --count 2>/dev/null || echo "0") AHEAD=$(git rev-list "origin/${PRIMARY_BRANCH}..HEAD" --count 2>/dev/null || echo "0")
HAS_CHANGES=$(git status --porcelain) HAS_CHANGES=$(git status --porcelain)
if [ -n "$REFUSAL_JSON" ] && [ "$AHEAD" -eq 0 ] && [ -z "$HAS_CHANGES" ]; then if [ -n "$REFUSAL_JSON" ] && [ "$AHEAD" -eq 0 ] && [ -z "$HAS_CHANGES" ]; then
@ -839,7 +839,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
--arg title "fix: ${ISSUE_TITLE} (#${ISSUE})" \ --arg title "fix: ${ISSUE_TITLE} (#${ISSUE})" \
--rawfile body "/tmp/pr-body-${ISSUE}.txt" \ --rawfile body "/tmp/pr-body-${ISSUE}.txt" \
--arg head "$BRANCH" \ --arg head "$BRANCH" \
--arg base "master" \ --arg base "${PRIMARY_BRANCH}" \
'{title: $title, body: $body, head: $head, base: $base}' > /tmp/pr-request-${ISSUE}.json '{title: $title, body: $body, head: $head, base: $base}' > /tmp/pr-request-${ISSUE}.json
PR_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ PR_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
@ -894,7 +894,7 @@ do_merge() {
if [ "$mergeable" = "false" ]; then if [ "$mergeable" = "false" ]; then
log "PR #${PR_NUMBER} has merge conflicts — attempting rebase" log "PR #${PR_NUMBER} has merge conflicts — attempting rebase"
local work_dir="${WORKTREE:-$REPO_ROOT}" local work_dir="${WORKTREE:-$REPO_ROOT}"
if (cd "$work_dir" && git fetch origin master && git rebase origin/master 2>&1); then if (cd "$work_dir" && git fetch origin "${PRIMARY_BRANCH}" && git rebase "origin/${PRIMARY_BRANCH}" 2>&1); then
log "rebase succeeded — force pushing" log "rebase succeeded — force pushing"
(cd "$work_dir" && git push origin "${BRANCH}" --force-with-lease 2>&1) || true (cd "$work_dir" && git push origin "${BRANCH}" --force-with-lease 2>&1) || true
# Wait for CI on the new commit # Wait for CI on the new commit
@ -925,8 +925,8 @@ do_merge() {
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
log "PR #${PR_NUMBER} merged!" log "PR #${PR_NUMBER} merged!"
# Update STATE.md on master (pull merged changes first) # Update STATE.md on primary branch (pull merged changes first)
(cd "$REPO_ROOT" && git checkout master 2>/dev/null && git pull --ff-only origin master 2>/dev/null) || true (cd "$REPO_ROOT" && git checkout "${PRIMARY_BRANCH}" 2>/dev/null && git pull --ff-only origin "${PRIMARY_BRANCH}" 2>/dev/null) || true
append_state_log || log "WARNING: STATE.md update failed (non-fatal)" append_state_log || log "WARNING: STATE.md update failed (non-fatal)"
curl -sf -X DELETE \ curl -sf -X DELETE \
@ -995,7 +995,7 @@ while [ "$REVIEW_ROUND" -lt "$MAX_REVIEW_ROUNDS" ]; do
if [ -n "$PIPELINE_NUM" ]; then if [ -n "$PIPELINE_NUM" ]; then
FAILED_INFO=$(curl -sf \ FAILED_INFO=$(curl -sf \
-H "Authorization: Bearer ${WOODPECKER_TOKEN}" \ -H "Authorization: Bearer ${WOODPECKER_TOKEN}" \
"http://localhost:8000/api/repos/2/pipelines/${PIPELINE_NUM}" | \ "${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}/pipelines/${PIPELINE_NUM}" | \
jq -r '.workflows[]?.children[]? | select(.state=="failure") | "\(.name)|\(.exit_code)"' | head -1) jq -r '.workflows[]?.children[]? | select(.state=="failure") | "\(.name)|\(.exit_code)"' | head -1)
FAILED_STEP=$(echo "$FAILED_INFO" | cut -d'|' -f1) FAILED_STEP=$(echo "$FAILED_INFO" | cut -d'|' -f1)
FAILED_EXIT=$(echo "$FAILED_INFO" | cut -d'|' -f2) FAILED_EXIT=$(echo "$FAILED_INFO" | cut -d'|' -f2)

View file

@ -2,7 +2,7 @@
# dev-poll.sh — Pull-based factory: find the next ready issue and start dev-agent # dev-poll.sh — Pull-based factory: find the next ready issue and start dev-agent
# #
# Pull system: issues labeled "backlog" are candidates. An issue is READY when # Pull system: issues labeled "backlog" are candidates. An issue is READY when
# ALL its dependency issues are closed AND their PRs are merged into master. # ALL its dependency issues are closed (and their PRs merged).
# No "todo" label needed — readiness is derived from reality. # No "todo" label needed — readiness is derived from reality.
# #
# Priority: # Priority:

View file

@ -1,6 +1,6 @@
# Factory Supervisor # Factory Supervisor
You are the factory supervisor for `johba/harb`. You were called because You are the factory supervisor for `$CODEBERG_REPO`. You were called because
`factory-poll.sh` detected an issue it couldn't auto-fix. `factory-poll.sh` detected an issue it couldn't auto-fix.
## Priority Order ## Priority Order
@ -34,9 +34,11 @@ source ${FACTORY_ROOT}/lib/env.sh
This gives you: This gives you:
- `codeberg_api GET "/pulls?state=open"` — Codeberg API (uses $CODEBERG_TOKEN) - `codeberg_api GET "/pulls?state=open"` — Codeberg API (uses $CODEBERG_TOKEN)
- `wpdb -c "SELECT ..."` — Woodpecker Postgres (uses $WOODPECKER_DB_PASSWORD) - `wpdb -c "SELECT ..."` — Woodpecker Postgres (uses $WOODPECKER_DB_PASSWORD)
- `woodpecker_api "/repos/2/pipelines"` — Woodpecker REST API (uses $WOODPECKER_TOKEN) - `woodpecker_api "/repos/$WOODPECKER_REPO_ID/pipelines"` — Woodpecker REST API (uses $WOODPECKER_TOKEN)
- `$REVIEW_BOT_TOKEN` — for posting reviews as the review_bot account - `$REVIEW_BOT_TOKEN` — for posting reviews as the review_bot account
- `$HARB_REPO_ROOT` — path to the harb repo - `$PROJECT_REPO_ROOT` — path to the target project repo
- `$PROJECT_NAME` — short project name (for worktree prefixes, container names)
- `$PRIMARY_BRANCH` — main branch (master or main)
- `$FACTORY_ROOT` — path to the dark-factory repo - `$FACTORY_ROOT` — path to the dark-factory repo
## Escalation ## Escalation

View file

@ -4,15 +4,15 @@
- Woodpecker CI at localhost:8000 (Docker backend) - Woodpecker CI at localhost:8000 (Docker backend)
- Postgres DB: use `wpdb` helper from env.sh - Postgres DB: use `wpdb` helper from env.sh
- Woodpecker API: use `woodpecker_api` helper from env.sh - Woodpecker API: use `woodpecker_api` helper from env.sh
- CI images: pre-built at `registry.niovi.voyage/harb/*:latest` - Example (harb): CI images pre-built at `registry.niovi.voyage/harb/*:latest`
## Safe Fixes ## Safe Fixes
- Retrigger CI: push empty commit to PR branch - Retrigger CI: push empty commit to PR branch
```bash ```bash
cd /tmp/harb-worktree-<issue> && git commit --allow-empty -m "ci: retrigger" --no-verify && git push origin <branch> --force cd /tmp/${PROJECT_NAME}-worktree-<issue> && git commit --allow-empty -m "ci: retrigger" --no-verify && git push origin <branch> --force
``` ```
- Restart woodpecker-agent: `sudo systemctl restart woodpecker-agent` - Restart woodpecker-agent: `sudo systemctl restart woodpecker-agent`
- View pipeline status: `wpdb -c "SELECT number, status FROM pipelines WHERE repo_id=2 ORDER BY number DESC LIMIT 5;"` - View pipeline status: `wpdb -c "SELECT number, status FROM pipelines WHERE repo_id=$WOODPECKER_REPO_ID ORDER BY number DESC LIMIT 5;"`
- View failed steps: `bash ${FACTORY_ROOT}/lib/ci-debug.sh failures <pipeline-number>` - View failed steps: `bash ${FACTORY_ROOT}/lib/ci-debug.sh failures <pipeline-number>`
- View step logs: `bash ${FACTORY_ROOT}/lib/ci-debug.sh logs <pipeline-number> <step-name>` - View step logs: `bash ${FACTORY_ROOT}/lib/ci-debug.sh logs <pipeline-number> <step-name>`
@ -23,7 +23,7 @@
## Known Issues ## Known Issues
- Codeberg rate-limits SSH clones. `git` step fails with exit 128. Retrigger usually works. - Codeberg rate-limits SSH clones. `git` step fails with exit 128. Retrigger usually works.
- `log_entries` table grows fast (was 5.6GB once). Truncate periodically. - `log_entries` table grows fast (was 5.6GB once). Truncate periodically.
- Running CI + harb stack = 14+ containers on 8GB. Memory pressure is real. - Example (harb): Running CI + harb stack = 14+ containers on 8GB. Memory pressure is real.
- CI images take hours to rebuild. Never run `docker system prune -a`. - CI images take hours to rebuild. Never run `docker system prune -a`.
## Lessons Learned ## Lessons Learned
@ -31,10 +31,10 @@
- Exit code 137 = OOM kill. Check memory, kill stale processes, retrigger. - Exit code 137 = OOM kill. Check memory, kill stale processes, retrigger.
- `node-quality` step fails on eslint/typescript errors — these need code fixes, not CI fixes. - `node-quality` step fails on eslint/typescript errors — these need code fixes, not CI fixes.
### FEE_DEST address must match DeployLocal.sol ### Example (harb): FEE_DEST address must match DeployLocal.sol
When DeployLocal.sol changes the feeDest address, bootstrap-common.sh must also be updated. When DeployLocal.sol changes the feeDest address, bootstrap-common.sh must also be updated.
Current feeDest = keccak256('harb.local.feeDest') = 0x8A9145E1Ea4C4d7FB08cF1011c8ac1F0e10F9383. Current feeDest = keccak256('harb.local.feeDest') = 0x8A9145E1Ea4C4d7FB08cF1011c8ac1F0e10F9383.
Symptom: bootstrap step exits 1 after 'Granting recenter access to deployer' with no error — setRecenterAccess reverts because wrong address is impersonated. Symptom: bootstrap step exits 1 after 'Granting recenter access to deployer' with no error — setRecenterAccess reverts because wrong address is impersonated.
### keccak-derived FEE_DEST requires anvil_setBalance before impersonation ### Example (harb): keccak-derived FEE_DEST requires anvil_setBalance before impersonation
When FEE_DEST is a keccak-derived address (e.g. keccak256('harb.local.feeDest')), it has zero ETH balance. Any function that calls `anvil_impersonateAccount` then `cast send --from $FEE_DEST --unlocked` will fail silently (output redirected to LOG_FILE) but exit 1 due to gas deduction failure. Fix: add `cast rpc anvil_setBalance "$FEE_DEST" "0xDE0B6B3A7640000"` before impersonation. Applied in both bootstrap-common.sh and red-team.sh. When FEE_DEST is a keccak-derived address (e.g. keccak256('harb.local.feeDest')), it has zero ETH balance. Any function that calls `anvil_impersonateAccount` then `cast send --from $FEE_DEST --unlocked` will fail silently (output redirected to LOG_FILE) but exit 1 due to gas deduction failure. Fix: add `cast rpc anvil_setBalance "$FEE_DEST" "0xDE0B6B3A7640000"` before impersonation. Applied in both bootstrap-common.sh and red-team.sh.

View file

@ -10,8 +10,8 @@ Codeberg rate-limits SSH and HTTPS clones. Symptoms:
- **Do NOT retrigger** during a rate-limit storm. Wait 10-15 minutes. - **Do NOT retrigger** during a rate-limit storm. Wait 10-15 minutes.
- Check if multiple pipelines failed on `git` step recently: - Check if multiple pipelines failed on `git` step recently:
```bash ```bash
wpdb -c "SELECT number, status, to_timestamp(started) FROM pipelines WHERE repo_id=2 AND status='failure' ORDER BY number DESC LIMIT 5;" wpdb -c "SELECT number, status, to_timestamp(started) FROM pipelines WHERE repo_id=$WOODPECKER_REPO_ID AND status='failure' ORDER BY number DESC LIMIT 5;"
wpdb -c "SELECT s.name, s.exit_code FROM steps s JOIN pipelines p ON s.pipeline_id=p.id WHERE p.number=<N> AND p.repo_id=2 AND s.state='failure';" wpdb -c "SELECT s.name, s.exit_code FROM steps s JOIN pipelines p ON s.pipeline_id=p.id WHERE p.number=<N> AND p.repo_id=$WOODPECKER_REPO_ID AND s.state='failure';"
``` ```
- If multiple `git` failures with exit 128 in the last 15 min → it's rate limiting. Wait. - If multiple `git` failures with exit 128 in the last 15 min → it's rate limiting. Wait.
- Only retrigger after 15+ minutes of no CI activity. - Only retrigger after 15+ minutes of no CI activity.

View file

@ -5,13 +5,13 @@
- `dev-agent.sh` uses `claude -p` for implementation, runs in git worktree - `dev-agent.sh` uses `claude -p` for implementation, runs in git worktree
- Lock file: `/tmp/dev-agent.lock` (contains PID) - Lock file: `/tmp/dev-agent.lock` (contains PID)
- Status file: `/tmp/dev-agent-status` - Status file: `/tmp/dev-agent-status`
- Worktrees: `/tmp/harb-worktree-<issue-number>/` - Worktrees: `/tmp/${PROJECT_NAME}-worktree-<issue-number>/`
## Safe Fixes ## Safe Fixes
- Remove stale lock: `rm -f /tmp/dev-agent.lock` (only if PID is dead) - Remove stale lock: `rm -f /tmp/dev-agent.lock` (only if PID is dead)
- Kill stuck agent: `kill <pid>` then clean lock - Kill stuck agent: `kill <pid>` then clean lock
- Restart on derailed PR: `bash ${FACTORY_ROOT}/dev/dev-agent.sh <issue-number> &` - Restart on derailed PR: `bash ${FACTORY_ROOT}/dev/dev-agent.sh <issue-number> &`
- Clean worktree: `cd /home/debian/harb && git worktree remove /tmp/harb-worktree-<N> --force` - Clean worktree: `cd $PROJECT_REPO_ROOT && git worktree remove /tmp/${PROJECT_NAME}-worktree-<N> --force`
- Remove `in-progress` label if agent died without cleanup: - Remove `in-progress` label if agent died without cleanup:
```bash ```bash
codeberg_api DELETE "/issues/<N>/labels/in-progress" codeberg_api DELETE "/issues/<N>/labels/in-progress"
@ -38,7 +38,7 @@
## Dependency Resolution ## Dependency Resolution
**Trust closed state.** If a dependency issue is closed, the code is on master. Period. **Trust closed state.** If a dependency issue is closed, the code is on the primary branch. Period.
DO NOT try to find the specific PR that closed an issue. This is over-engineering that causes false negatives: DO NOT try to find the specific PR that closed an issue. This is over-engineering that causes false negatives:
- Codeberg shares issue/PR numbering — no guaranteed relationship - Codeberg shares issue/PR numbering — no guaranteed relationship

View file

@ -3,14 +3,14 @@
## Safe Fixes ## Safe Fixes
- Docker cleanup: `sudo docker system prune -f` (keeps images, removes stopped containers + dangling layers) - Docker cleanup: `sudo docker system prune -f` (keeps images, removes stopped containers + dangling layers)
- Truncate factory logs >5MB: `truncate -s 0 <file>` - Truncate factory logs >5MB: `truncate -s 0 <file>`
- Remove stale worktrees: check `/tmp/harb-worktree-*`, only if dev-agent not running on them - Remove stale worktrees: check `/tmp/${PROJECT_NAME}-worktree-*`, only if dev-agent not running on them
- Woodpecker log_entries: `DELETE FROM log_entries WHERE id < (SELECT max(id) - 100000 FROM log_entries);` then `VACUUM;` - Woodpecker log_entries: `DELETE FROM log_entries WHERE id < (SELECT max(id) - 100000 FROM log_entries);` then `VACUUM;`
- Node module caches in worktrees: `rm -rf /tmp/harb-worktree-*/node_modules/` - Node module caches in worktrees: `rm -rf /tmp/${PROJECT_NAME}-worktree-*/node_modules/`
- Git garbage collection: `cd /home/debian/harb && git gc --prune=now` - Git garbage collection: `cd $PROJECT_REPO_ROOT && git gc --prune=now`
## Dangerous (escalate) ## Dangerous (escalate)
- `docker system prune -a --volumes` — deletes ALL images including CI build cache - `docker system prune -a --volumes` — deletes ALL images including CI build cache
- Deleting anything in `/home/debian/harb/` that's tracked by git - Deleting anything in `$PROJECT_REPO_ROOT/` that's tracked by git
- Truncating Woodpecker DB tables other than log_entries - Truncating Woodpecker DB tables other than log_entries
## Known Disk Hogs ## Known Disk Hogs

View file

@ -1,39 +1,39 @@
# Git Best Practices # Git Best Practices
## Environment ## Environment
- Repo: `/home/debian/harb`, remote: `codeberg.org/johba/harb` - Repo: `$PROJECT_REPO_ROOT`, remote: `$PROJECT_REMOTE`
- Branch: `master` (protected — no direct push, PRs only) - Branch: `$PRIMARY_BRANCH` (protected — no direct push, PRs only)
- Worktrees: `/tmp/harb-worktree-<issue>/` - Worktrees: `/tmp/${PROJECT_NAME}-worktree-<issue>/`
## Safe Fixes ## Safe Fixes
- Abort stale rebase: `cd /home/debian/harb && git rebase --abort` - Abort stale rebase: `cd $PROJECT_REPO_ROOT && git rebase --abort`
- Switch to master: `git checkout master` - Switch to $PRIMARY_BRANCH: `git checkout $PRIMARY_BRANCH`
- Prune worktrees: `git worktree prune` - Prune worktrees: `git worktree prune`
- Reset dirty state: `git checkout -- .` (only uncommitted changes) - Reset dirty state: `git checkout -- .` (only uncommitted changes)
- Fetch latest: `git fetch origin master` - Fetch latest: `git fetch origin $PRIMARY_BRANCH`
## Auto-fixable by Supervisor ## Auto-fixable by Supervisor
- **Merge conflict on approved PR**: rebase onto master and force-push - **Merge conflict on approved PR**: rebase onto $PRIMARY_BRANCH and force-push
```bash ```bash
cd /tmp/harb-worktree-<issue> || git worktree add /tmp/harb-worktree-<issue> <branch> cd /tmp/${PROJECT_NAME}-worktree-<issue> || git worktree add /tmp/${PROJECT_NAME}-worktree-<issue> <branch>
cd /tmp/harb-worktree-<issue> cd /tmp/${PROJECT_NAME}-worktree-<issue>
git fetch origin master git fetch origin $PRIMARY_BRANCH
git rebase origin/master git rebase origin/$PRIMARY_BRANCH
# If conflict is trivial (NatSpec, comments): resolve and continue # If conflict is trivial (NatSpec, comments): resolve and continue
# If conflict is code logic: escalate to Clawy # If conflict is code logic: escalate to Clawy
git push origin <branch> --force git push origin <branch> --force
``` ```
- **Stale rebase**: `git rebase --abort && git checkout master` - **Stale rebase**: `git rebase --abort && git checkout $PRIMARY_BRANCH`
- **Wrong branch**: `git checkout master` - **Wrong branch**: `git checkout $PRIMARY_BRANCH`
## Dangerous (escalate) ## Dangerous (escalate)
- `git reset --hard` on any branch with unpushed work - `git reset --hard` on any branch with unpushed work
- Deleting remote branches - Deleting remote branches
- Force-pushing to any branch - Force-pushing to any branch
- Anything on the master branch directly - Anything on the $PRIMARY_BRANCH branch directly
## Known Issues ## Known Issues
- Main repo MUST be on master at all times. Dev work happens in worktrees. - Main repo MUST be on $PRIMARY_BRANCH at all times. Dev work happens in worktrees.
- Stale rebases (detached HEAD) break all worktree creation — silent factory stall. - Stale rebases (detached HEAD) break all worktree creation — silent factory stall.
- `git worktree add` fails if target directory exists (even empty). Remove first. - `git worktree add` fails if target directory exists (even empty). Remove first.
- Many old branches exist locally (100+). Normal — don't bulk-delete. - Many old branches exist locally (100+). Normal — don't bulk-delete.

View file

@ -7,12 +7,12 @@
## Safe Fixes (no permission needed) ## Safe Fixes (no permission needed)
- Kill stale `claude` processes (>3h old): `pgrep -f "claude" --older 10800 | xargs kill` - Kill stale `claude` processes (>3h old): `pgrep -f "claude" --older 10800 | xargs kill`
- Drop filesystem caches: `sync && echo 3 | sudo tee /proc/sys/vm/drop_caches` - Drop filesystem caches: `sync && echo 3 | sudo tee /proc/sys/vm/drop_caches`
- Restart bloated Anvil: `sudo docker restart harb-anvil-1` (grows to 12GB+ over hours) - Restart bloated Anvil: `sudo docker restart ${PROJECT_NAME}-anvil-1` (grows to 12GB+ over hours)
- Kill orphan node processes from dead worktrees - Kill orphan node processes from dead worktrees
## Dangerous (escalate) ## Dangerous (escalate)
- `docker system prune -a --volumes` — kills CI images, hours to rebuild - `docker system prune -a --volumes` — kills CI images, hours to rebuild
- Stopping harb stack containers — breaks dev environment - Stopping project stack containers — breaks dev environment
- OOM that survives all safe fixes — needs human decision on what to kill - OOM that survives all safe fixes — needs human decision on what to kill
## Known Memory Hogs ## Known Memory Hogs
@ -26,4 +26,4 @@
## Lessons Learned ## Lessons Learned
- After killing processes, always `sync && echo 3 | sudo tee /proc/sys/vm/drop_caches` - After killing processes, always `sync && echo 3 | sudo tee /proc/sys/vm/drop_caches`
- Swap doesn't drain from dropping caches alone — it's actual paged-out process memory - Swap doesn't drain from dropping caches alone — it's actual paged-out process memory
- Running CI + full harb stack = 14+ containers on 8GB. Only one pipeline at a time. - Running CI + full project stack = 14+ containers on 8GB. Only one pipeline at a time.

View file

@ -75,9 +75,10 @@ if [ "${AVAIL_MB:-9999}" -lt 500 ] || { [ "${SWAP_USED_MB:-0}" -gt 3000 ] && [ "
fixed "Dropped filesystem caches" fixed "Dropped filesystem caches"
# Restart Anvil if it's bloated (>1GB RSS) # Restart Anvil if it's bloated (>1GB RSS)
ANVIL_RSS=$(sudo docker stats harb-anvil-1 --no-stream --format '{{.MemUsage}}' 2>/dev/null | grep -oP '^\S+' | head -1 || echo "0") ANVIL_CONTAINER="${ANVIL_CONTAINER:-${PROJECT_NAME}-anvil-1}"
ANVIL_RSS=$(sudo docker stats "$ANVIL_CONTAINER" --no-stream --format '{{.MemUsage}}' 2>/dev/null | grep -oP '^\S+' | head -1 || echo "0")
if echo "$ANVIL_RSS" | grep -qP '\dGiB'; then if echo "$ANVIL_RSS" | grep -qP '\dGiB'; then
sudo docker restart harb-anvil-1 >/dev/null 2>&1 && fixed "Restarted bloated Anvil (${ANVIL_RSS})" sudo docker restart "$ANVIL_CONTAINER" >/dev/null 2>&1 && fixed "Restarted bloated Anvil (${ANVIL_RSS})"
fi fi
# Re-check after fixes # Re-check after fixes
@ -116,12 +117,12 @@ if [ "${DISK_PERCENT:-0}" -gt 80 ]; then
done done
# Clean old worktrees # Clean old worktrees
IDLE_WORKTREES=$(find /tmp/harb-worktree-* -maxdepth 0 -mmin +360 2>/dev/null || true) IDLE_WORKTREES=$(find /tmp/${PROJECT_NAME}-worktree-* -maxdepth 0 -mmin +360 2>/dev/null || true)
if [ -n "$IDLE_WORKTREES" ]; then if [ -n "$IDLE_WORKTREES" ]; then
cd "${HARB_REPO_ROOT}" && git worktree prune 2>/dev/null cd "${PROJECT_REPO_ROOT}" && git worktree prune 2>/dev/null
for wt in $IDLE_WORKTREES; do for wt in $IDLE_WORKTREES; do
# Only remove if dev-agent is not running on it # Only remove if dev-agent is not running on it
ISSUE_NUM=$(basename "$wt" | sed 's/harb-worktree-//') ISSUE_NUM=$(basename "$wt" | sed "s/${PROJECT_NAME}-worktree-//")
if ! pgrep -f "dev-agent.sh ${ISSUE_NUM}" >/dev/null 2>&1; then if ! pgrep -f "dev-agent.sh ${ISSUE_NUM}" >/dev/null 2>&1; then
rm -rf "$wt" && fixed "Removed stale worktree: $wt" rm -rf "$wt" && fixed "Removed stale worktree: $wt"
fi fi
@ -153,10 +154,10 @@ fi
status "P2: checking factory" status "P2: checking factory"
# CI stuck # CI stuck
STUCK_CI=$(wpdb -c "SELECT count(*) FROM pipelines WHERE repo_id=2 AND status='running' AND EXTRACT(EPOCH FROM now() - to_timestamp(started)) > 1200;" 2>/dev/null | xargs) STUCK_CI=$(wpdb -c "SELECT count(*) FROM pipelines WHERE repo_id=${WOODPECKER_REPO_ID} AND status='running' AND EXTRACT(EPOCH FROM now() - to_timestamp(started)) > 1200;" 2>/dev/null | xargs)
[ "${STUCK_CI:-0}" -gt 0 ] && p2 "CI: ${STUCK_CI} pipeline(s) running >20min" [ "${STUCK_CI:-0}" -gt 0 ] && p2 "CI: ${STUCK_CI} pipeline(s) running >20min"
PENDING_CI=$(wpdb -c "SELECT count(*) FROM pipelines WHERE repo_id=2 AND status='pending' AND EXTRACT(EPOCH FROM now() - to_timestamp(created)) > 1800;" 2>/dev/null | xargs) PENDING_CI=$(wpdb -c "SELECT count(*) FROM pipelines WHERE repo_id=${WOODPECKER_REPO_ID} AND status='pending' AND EXTRACT(EPOCH FROM now() - to_timestamp(created)) > 1800;" 2>/dev/null | xargs)
[ "${PENDING_CI:-0}" -gt 0 ] && p2 "CI: ${PENDING_CI} pipeline(s) pending >30min" [ "${PENDING_CI:-0}" -gt 0 ] && p2 "CI: ${PENDING_CI} pipeline(s) pending >30min"
# Dev-agent health # Dev-agent health
@ -177,19 +178,19 @@ if [ -f "$DEV_LOCK" ]; then
fi fi
# Git repo health # Git repo health
cd "${HARB_REPO_ROOT}" 2>/dev/null || true cd "${PROJECT_REPO_ROOT}" 2>/dev/null || true
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
GIT_REBASE=$([ -d .git/rebase-merge ] || [ -d .git/rebase-apply ] && echo "yes" || echo "no") GIT_REBASE=$([ -d .git/rebase-merge ] || [ -d .git/rebase-apply ] && echo "yes" || echo "no")
if [ "$GIT_REBASE" = "yes" ]; then if [ "$GIT_REBASE" = "yes" ]; then
git rebase --abort 2>/dev/null && git checkout master 2>/dev/null && \ git rebase --abort 2>/dev/null && git checkout "${PRIMARY_BRANCH}" 2>/dev/null && \
fixed "Aborted stale rebase, switched to master" || \ fixed "Aborted stale rebase, switched to ${PRIMARY_BRANCH}" || \
p2 "Git: stale rebase, auto-abort failed" p2 "Git: stale rebase, auto-abort failed"
fi fi
if [ "$GIT_BRANCH" != "master" ] && [ "$GIT_BRANCH" != "unknown" ]; then if [ "$GIT_BRANCH" != "${PRIMARY_BRANCH}" ] && [ "$GIT_BRANCH" != "unknown" ]; then
git checkout master 2>/dev/null && \ git checkout "${PRIMARY_BRANCH}" 2>/dev/null && \
fixed "Switched main repo from '${GIT_BRANCH}' to master" || \ fixed "Switched main repo from '${GIT_BRANCH}' to ${PRIMARY_BRANCH}" || \
p2 "Git: on '${GIT_BRANCH}' instead of master" p2 "Git: on '${GIT_BRANCH}' instead of ${PRIMARY_BRANCH}"
fi fi
# ============================================================================= # =============================================================================

View file

@ -151,7 +151,7 @@ log "Invoking claude -p for grooming"
# Build issue summary for context (titles + labels + deps) # Build issue summary for context (titles + labels + deps)
ISSUE_SUMMARY=$(echo "$ISSUES_JSON" | jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"') ISSUE_SUMMARY=$(echo "$ISSUES_JSON" | jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"')
PROMPT="You are harb's issue gardener. Your job: keep the backlog clean, well-structured, and actionable. PROMPT="You are the issue gardener for ${CODEBERG_REPO}. Your job: keep the backlog clean, well-structured, and actionable.
## Current open issues ## Current open issues
$ISSUE_SUMMARY $ISSUE_SUMMARY
@ -161,18 +161,15 @@ $(echo -e "$PROBLEMS")
## Tools available ## Tools available
- Codeberg API: use curl with the CODEBERG_TOKEN env var (already set in your environment) - Codeberg API: use curl with the CODEBERG_TOKEN env var (already set in your environment)
- Base URL: https://codeberg.org/api/v1/repos/johba/harb - Base URL: ${CODEBERG_API}
- Read issue: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" 'https://codeberg.org/api/v1/repos/johba/harb/issues/{number}' | jq '.body'\` - Read issue: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body'\`
- Relabel: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' 'https://codeberg.org/api/v1/repos/johba/harb/issues/{number}/labels' -d '{\"labels\":[652336]}'\` (652336=backlog, 1219499=tech-debt) - Relabel: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}'\`
- Comment: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' 'https://codeberg.org/api/v1/repos/johba/harb/issues/{number}/comments' -d '{\"body\":\"...\"}'\` - Comment: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'\`
- Close: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' 'https://codeberg.org/api/v1/repos/johba/harb/issues/{number}' -d '{\"state\":\"closed\"}'\` - Close: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"state\":\"closed\"}'\`
- Edit body: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' 'https://codeberg.org/api/v1/repos/johba/harb/issues/{number}' -d '{\"body\":\"new body\"}'\` - Edit body: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"body\":\"new body\"}'\`
- List labels: \`curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels'\` (to find label IDs)
- NEVER echo, log, or include the actual token value in any output — always reference \$CODEBERG_TOKEN - NEVER echo, log, or include the actual token value in any output — always reference \$CODEBERG_TOKEN
- You're running in the harb repo root. Read these before making decisions: - You're running in the project repo root. Read README.md and any docs/ files before making decisions.
- docs/PRODUCT-TRUTH.md — what the protocol is, key mechanics
- docs/ARCHITECTURE.md — file structure, packages, how things connect
- AGENTS.md — repo conventions, dev-agent expectations
- tools/push3-transpiler/README.md — Push3 instruction set (for seed issues)
## Primary mission: promote tech-debt → backlog ## Primary mission: promote tech-debt → backlog
Most open issues are raw review-bot findings labeled \`tech-debt\`. Your main job is to convert them into well-structured \`backlog\` items the dev-agent can execute. For each tech-debt issue: Most open issues are raw review-bot findings labeled \`tech-debt\`. Your main job is to convert them into well-structured \`backlog\` items the dev-agent can execute. For each tech-debt issue:
@ -212,7 +209,7 @@ ESCALATE
- If an issue is ambiguous or needs a design decision, ESCALATE it — don't skip it silently. - If an issue is ambiguous or needs a design decision, ESCALATE it — don't skip it silently.
- Every tech-debt issue in the list above should result in either an ACTION (promoted) or an ESCALATE (needs decision). Never skip silently." - Every tech-debt issue in the list above should result in either an ACTION (promoted) or an ESCALATE (needs decision). Never skip silently."
CLAUDE_OUTPUT=$(cd /home/debian/harb && CODEBERG_TOKEN="$CODEBERG_TOKEN" timeout "$CLAUDE_TIMEOUT" \ CLAUDE_OUTPUT=$(cd "${PROJECT_REPO_ROOT}" && CODEBERG_TOKEN="$CODEBERG_TOKEN" timeout "$CLAUDE_TIMEOUT" \
claude -p "$PROMPT" \ claude -p "$PROMPT" \
--model sonnet \ --model sonnet \
--dangerously-skip-permissions \ --dangerously-skip-permissions \

View file

@ -12,10 +12,9 @@ set -euo pipefail
# Load shared environment # Load shared environment
source "$(dirname "$0")/../lib/env.sh" source "$(dirname "$0")/../lib/env.sh"
export WOODPECKER_SERVER="http://localhost:8000"
# WOODPECKER_TOKEN loaded from .env via env.sh # WOODPECKER_TOKEN loaded from .env via env.sh
REPO="johba/harb" REPO="${CODEBERG_REPO}"
API="${WOODPECKER_SERVER}/api/repos/2" API="${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}"
api() { api() {
curl -sf -H "Authorization: Bearer ${WOODPECKER_TOKEN}" "${API}/$1" curl -sf -H "Authorization: Bearer ${WOODPECKER_TOKEN}" "${API}/$1"

View file

@ -25,10 +25,14 @@ if [ -z "${CODEBERG_TOKEN:-}" ]; then
fi fi
export CODEBERG_TOKEN export CODEBERG_TOKEN
# Defaults # Project config
export CODEBERG_REPO="${CODEBERG_REPO:-johba/harb}" export CODEBERG_REPO="${CODEBERG_REPO:-johba/harb}"
export CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1/repos/${CODEBERG_REPO}}" export CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1/repos/${CODEBERG_REPO}}"
export HARB_REPO_ROOT="${HARB_REPO_ROOT:-/home/debian/harb}" export PROJECT_NAME="${PROJECT_NAME:-${CODEBERG_REPO##*/}}"
export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-${HARB_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}}"
export HARB_REPO_ROOT="${PROJECT_REPO_ROOT}" # deprecated alias
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
export WOODPECKER_REPO_ID="${WOODPECKER_REPO_ID:-2}"
export WOODPECKER_SERVER="${WOODPECKER_SERVER:-http://localhost:8000}" export WOODPECKER_SERVER="${WOODPECKER_SERVER:-http://localhost:8000}"
export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}" export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}"

View file

@ -1,15 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# review-poll.sh — Poll open PRs and review those with green CI # review-poll.sh — Poll open PRs and review those with green CI
# #
# Peek while running: cat /tmp/harb-review-status # Peek while running: cat /tmp/<project>-review-status
# Full log: tail -f ~/scripts/harb-review/review.log # Full log: tail -f <factory-root>/review/review.log
set -euo pipefail set -euo pipefail
# Load shared environment # Load shared environment
source "$(dirname "$0")/../lib/env.sh" source "$(dirname "$0")/../lib/env.sh"
export HOME="${HOME:-/home/debian}"
REPO="${CODEBERG_REPO}" REPO="${CODEBERG_REPO}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@ -35,10 +34,10 @@ log "--- Poll start ---"
PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ PRS=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
"${API_BASE}/pulls?state=open&limit=20" | \ "${API_BASE}/pulls?state=open&limit=20" | \
jq -r '.[] | select(.base.ref == "master") | select(.draft != true) | select(.title | test("^\\[?WIP[\\]:]"; "i") | not) | "\(.number) \(.head.sha)"') jq -r --arg branch "${PRIMARY_BRANCH}" '.[] | select(.base.ref == $branch) | select(.draft != true) | select(.title | test("^\\[?WIP[\\]:]"; "i") | not) | "\(.number) \(.head.sha)"')
if [ -z "$PRS" ]; then if [ -z "$PRS" ]; then
log "No open PRs targeting master" log "No open PRs targeting ${PRIMARY_BRANCH}"
exit 0 exit 0
fi fi

View file

@ -9,8 +9,8 @@
# - Auto-creates follow-up issues for pre-existing bugs flagged by reviewer # - Auto-creates follow-up issues for pre-existing bugs flagged by reviewer
# - JSON output format with validation + retry # - JSON output format with validation + retry
# #
# Peek while running: cat /tmp/harb-review-status # Peek while running: cat /tmp/<project>-review-status
# Watch log: tail -f ~/scripts/harb-review/review.log # Watch log: tail -f <factory-root>/review/review.log
set -euo pipefail set -euo pipefail
@ -21,12 +21,12 @@ source "$(dirname "$0")/../lib/env.sh"
PR_NUMBER="${1:?Usage: review-pr.sh <pr-number> [--force]}" PR_NUMBER="${1:?Usage: review-pr.sh <pr-number> [--force]}"
FORCE="${2:-}" FORCE="${2:-}"
REPO="${CODEBERG_REPO}" REPO="${CODEBERG_REPO}"
REPO_ROOT="/home/debian/harb" REPO_ROOT="${PROJECT_REPO_ROOT}"
# Bot account for posting reviews (separate user required for branch protection approvals) # Bot account for posting reviews (separate user required for branch protection approvals)
API_BASE="${CODEBERG_API}" API_BASE="${CODEBERG_API}"
LOCKFILE="/tmp/harb-review.lock" LOCKFILE="/tmp/${PROJECT_NAME}-review.lock"
STATUSFILE="/tmp/harb-review-status" STATUSFILE="/tmp/${PROJECT_NAME}-review-status"
LOGDIR="${FACTORY_ROOT}/review" LOGDIR="${FACTORY_ROOT}/review"
LOGFILE="$LOGDIR/review.log" LOGFILE="$LOGDIR/review.log"
MIN_MEM_MB=1500 MIN_MEM_MB=1500
@ -91,8 +91,8 @@ log "${PR_TITLE} (${PR_HEAD}→${PR_BASE} ${PR_SHA:0:7})"
if [ "$PR_STATE" != "open" ]; then if [ "$PR_STATE" != "open" ]; then
log "SKIP: state=${PR_STATE}" log "SKIP: state=${PR_STATE}"
cd "$REPO_ROOT" cd "$REPO_ROOT"
git worktree remove "/tmp/harb-review-${PR_NUMBER}" --force 2>/dev/null || true git worktree remove "/tmp/${PROJECT_NAME}-review-${PR_NUMBER}" --force 2>/dev/null || true
rm -rf "/tmp/harb-review-${PR_NUMBER}" 2>/dev/null || true rm -rf "/tmp/${PROJECT_NAME}-review-${PR_NUMBER}" 2>/dev/null || true
exit 0 exit 0
fi fi
@ -181,7 +181,7 @@ fi
status "checking out PR branch" status "checking out PR branch"
cd "$REPO_ROOT" cd "$REPO_ROOT"
git fetch origin "$PR_HEAD" 2>/dev/null || true git fetch origin "$PR_HEAD" 2>/dev/null || true
REVIEW_WORKTREE="/tmp/harb-review-${PR_NUMBER}" REVIEW_WORKTREE="/tmp/${PROJECT_NAME}-review-${PR_NUMBER}"
if [ -d "$REVIEW_WORKTREE" ]; then if [ -d "$REVIEW_WORKTREE" ]; then
cd "$REVIEW_WORKTREE" cd "$REVIEW_WORKTREE"
@ -340,7 +340,7 @@ DEVRESP_EOF
${INCREMENTAL_DIFF} ${INCREMENTAL_DIFF}
\`\`\` \`\`\`
### Full Diff (master..${PR_SHA:0:7}) ### Full Diff (${PRIMARY_BRANCH}..${PR_SHA:0:7})
\`\`\`diff \`\`\`diff
${DIFF} ${DIFF}
\`\`\` \`\`\`