gendesign/.claude/agents/_autonomous_pickup.md
lekss361 e4dc866e20 fixup(claude): address review-bot blockers on PR #610
🔴 Critical (3):

1. cleanup-stale-claims.sh: export CUTOFF + STALE_HOURS
   Без `export` Python heredoc child process получал "0" вместо реального
   cutoff → cron всегда видел все issues как fresh → ничего не освобождал.
   Plus added abort при CUTOFF=0 в Python heredoc для defense-in-depth.

2. cleanup-stale-claims.sh: datetime.UTC → datetime.timezone.utc
   datetime.UTC требует Python 3.11+. Forgejo runner ubuntu-latest имеет 3.10.
   datetime.timezone.utc works в 3.8+.

3. cleanup-stale-claims.sh: invert label order — DELETE wip FIRST, потом
   POST ready. Если step 2 (add ready) fails — issue в neutral state без
   status/*, менее опасно чем оба status/ready+status/wip одновременно.

🟠 + 🟡 + 🟢 (5):

4. Added pagination loop (defensive cap 10 pages × 50 = 500 issues), но
   реализована через temp file + Python parsing (не bash JSON concat).

5. Removed dead code: iso_to_epoch() shell helper не использовался.

6. _autonomous_pickup.md: fix broken `Out-Null | if (-not $?)` pattern.
   GetEnvironmentVariable НЕ throws при missing — возвращает $null. $? у
   Get* всегда $true. Заменено на прямую проверку результата.

7. stale-claims.yml: добавлен concurrency group чтобы overlapping runs
   не stomp'ились (хотя 5-min timeout делает overlap unlikely, defensive).

8. Documented updated_at != heartbeat assumption в header script'а.

Не сделано (low/optional):

- Setup script tokens в memory (UX trade-off — не critical)
- Token suffix в console output (минор info leak в shell history)

Refs: PR #610 review by review-bot (sha=297eb29, verdict=changes)
2026-05-28 00:33:27 +03:00

9.8 KiB
Raw Blame History

name description status created_at
_autonomous_pickup [SHARED SNIPPET — do not invoke directly] Forgejo queue pickup logic, импортируется во все auto-*.md агенты. Содержит claim/state-transition contract. draft 2026-05-27

Autonomous queue pickup — shared contract

NOT a standalone agent. Этот файл — общая инструкция, которую копи-пастят внутрь каждого auto-*.md. Содержит claim/lifecycle/kill-switch логику.

Pre-flight checklist (ОБЯЗАТЕЛЬНО до /loop запуска)

Это критично. Без правильной настройки git identity → commits будут писаться под user'ом (lekss361), не под ботом. Audit trail сломается.

Шаг 1 — Где живут PAT'ы

PAT'ы хранятся в двух местах одновременно:

  1. Vault meta/00_credentials.md — sensitive backup (read-only reference)
  2. Windows User-scope env vars — production-ready, persistent:
    • FORGEJO_TOKEN_ANALYST
    • FORGEJO_TOKEN_BACKEND
    • FORGEJO_TOKEN_FRONTEND
    • FORGEJO_TOKEN_REVIEWER
    • FORGEJO_TOKEN_QA
    • FORGEJO_URL_BOTS = https://git.gendsgn.ru
    • FORGEJO_REPO_BOTS = lekss361/gendesign

Setup один раз через PowerShell (см. scripts/setup-bot-env.ps1). После этого env vars доступны во всех новых shell-сессиях автоматически — никаких Get-Content / файлов.

Проверь что выставлены:

# GetEnvironmentVariable возвращает $null если var отсутствует — НЕ throws.
# Поэтому проверяем результат напрямую (не через $? — он у Get* всегда $true).
if (-not [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User")) {
    Write-Error "❌ FORGEJO_TOKEN_BACKEND не выставлен. Запусти scripts/setup-bot-env.ps1 сначала"
}

Шаг 2 — Env vars + git identity (per окно)

Замени <ROLE> на свою роль (ANALYST/BACKEND/FRONTEND/REVIEWER/QA — UPPER-case):

$ROLE = "BACKEND"   # ← ИЗМЕНИ ПЕРЕД ЗАПУСКОМ (UPPER-case)
$BOT  = "bot-$($ROLE.ToLower())"

# Resolve token из persistent User env
$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_$ROLE", "User")
$env:BOT_USERNAME  = $BOT
$env:FORGEJO_URL   = [System.Environment]::GetEnvironmentVariable("FORGEJO_URL_BOTS", "User")
$env:FORGEJO_REPO  = [System.Environment]::GetEnvironmentVariable("FORGEJO_REPO_BOTS", "User")

# Sanity: token не пустой
if (-not $env:FORGEJO_TOKEN) {
    Write-Error "❌ FORGEJO_TOKEN_$ROLE не выставлен. Запусти scripts/setup-bot-env.ps1"
    return
}

# Git identity — КРИТИЧНО, иначе commit author будет user'а (lekss361)
$env:GIT_AUTHOR_NAME     = $BOT
$env:GIT_AUTHOR_EMAIL    = "$BOT@gendsgn.local"
$env:GIT_COMMITTER_NAME  = $BOT
$env:GIT_COMMITTER_EMAIL = "$BOT@gendsgn.local"

Шаг 3 — Bot-remote (для git push audit-log)

Существующий forgejo remote использует lekss361's PAT — push через него запишется в Forgejo audit log как lekss361. Создай отдельный bot-remote:

git remote remove forgejo-bot 2>$null
git remote add forgejo-bot "https://$($env:BOT_USERNAME):$($env:FORGEJO_TOKEN)@git.gendsgn.ru/lekss361/gendesign.git"

# Везде в workflow:
#   git push forgejo-bot feat/X            (НЕ git push forgejo)

Шаг 4 — Sanity check (verify identity)

# 4a. PAT принадлежит правильному боту
$me = curl -sS -H "Authorization: token $env:FORGEJO_TOKEN" "$env:FORGEJO_URL/api/v1/user" | ConvertFrom-Json
if ($me.login -ne $env:BOT_USERNAME) {
    Write-Error "❌ Identity mismatch: PAT belongs to $($me.login), expected $env:BOT_USERNAME"
    exit 1
}
Write-Host "✓ PAT belongs to $($me.login)"

# 4b. Git identity (на сессию)
Write-Host "✓ Commits will be authored as: $env:GIT_AUTHOR_NAME <$env:GIT_AUTHOR_EMAIL>"

# 4c. Bot-remote configured
git remote -v | Select-String "forgejo-bot"

Только после 4a/4b/4c ✓ — запускай /loop.

Forgejo API endpoints (использует env vars из Шага 2)

curl_forgejo() {
  curl -sS -H "Authorization: token $FORGEJO_TOKEN" \
       -H "Content-Type: application/json" \
       "$FORGEJO_URL/api/v1/$1" "${@:2}"
}

Kill-switch check (выполняй ПЕРВЫМ делом в каждом /loop tick)

# Проверка через meta-issue с label "pause-bots"
if curl_forgejo "repos/$FORGEJO_REPO/issues?labels=pause-bots&state=open&limit=1" \
   | jq -e 'length > 0' > /dev/null; then
  echo "result: paused (pause-bots active)"
  exit 0  # /loop спит до следующего тика
fi

Pickup query — по scope

SCOPE="backend"  # ∈ {backend, frontend, db, qa, devops}

NEXT=$(curl_forgejo \
  "repos/$FORGEJO_REPO/issues?state=open&labels=scope/$SCOPE,status/ready&assigned_to=none&sort=newest&limit=1" \
  | jq -r '.[0] | if . then [.number, .title] | @tsv else "" end')

if [[ -z "$NEXT" ]]; then
  echo "result: idle, no work for scope/$SCOPE"
  exit 0
fi

Claim — atomic-ish через label transition

Race window note: Forgejo не поддерживает conditional-update (ETag/If-Match для issue PATCH). Между шагом 1 и 3 другой worker теоретически может тоже claim'нуть. Verify checks length == 1 AND .assignees[0] == me — иначе откатываемся.

ISSUE=$(echo "$NEXT" | cut -f1)

# 1. Assign self (atomic на стороне Forgejo для самого set-assignees, но не для transition)
curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE" -X PATCH \
  -d "{\"assignees\": [\"$BOT_USERNAME\"]}"

# 2. Transition ready → wip
curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE/labels" -X POST \
  -d '{"labels": ["status/wip"]}'
curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE/labels/status/ready" -X DELETE

# 3. Verify claim не перехвачен — STRICT check
ISSUE_JSON=$(curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE")
ASSIGNEE_COUNT=$(echo "$ISSUE_JSON" | jq '.assignees | length')
ASSIGNEE=$(echo "$ISSUE_JSON" | jq -r '.assignees[0].login // ""')

if [[ "$ASSIGNEE_COUNT" != "1" || "$ASSIGNEE" != "$BOT_USERNAME" ]]; then
  echo "result: lost race for #$ISSUE (assignees=$ASSIGNEE_COUNT, first=$ASSIGNEE) — releasing"
  # Best-effort rollback — снять wip, вернуть ready (не критично если не получится)
  curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE/labels" -X POST \
    -d '{"labels": ["status/ready"]}'
  curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE/labels/status/wip" -X DELETE
  exit 0
fi

State transitions reference

От → К Кто переключает Условие
(new) → status/ready auto-analyst issue декомпозирован, deps удовлетворены
status/readystatus/wip auto-backend / auto-frontend claim успешный
status/wipstatus/review worker PR открыт
status/reviewstatus/qa auto-code-reviewer APPROVE + merge
status/reviewstatus/blocked auto-code-reviewer BLOCK/FIX verdict
status/qastatus/done auto-qa-tester smoke OK, issue closed
status/qastatus/blocked auto-qa-tester smoke FAIL
status/blockedstatus/ready only human manual unblock
любой + pause-bots присутствует (никто не работает) kill-switch

Pause-bots поведение mid-work

Если pause-bots label появился ПОКА worker уже в wip:

  1. НЕ abort — finish текущий commit + push (минимизирует потерю работы)
  2. Open PR как обычно → PR попадёт в queue status/review (но reviewer тоже paused → PR не merge'нётся)
  3. result: PR #N opened, then paused due to kill-switch
  4. После un-pause — reviewer подхватит PR

Это НЕ release claim на исходный issue — он остаётся wip+assigned до merge.

Stale-claim cleanup — TODO (отдельный PR / cron)

⚠️ НЕ имплементировано в текущем PR. Без stale cleanup worker crash = issue застрянет в status/wip forever. До имплементации — мониторь вручную.

# Каждые 30 минут — освобождать issues застрявшие в wip >4h
curl_forgejo "repos/$FORGEJO_REPO/issues?labels=status/wip&state=open" \
  | jq -r '.[] | select(.updated_at < (now - 4*3600 | todate)) | .number' \
  | while read i; do
      curl_forgejo "repos/$FORGEJO_REPO/issues/$i" -X PATCH -d '{"assignees": []}'
      curl_forgejo "repos/$FORGEJO_REPO/issues/$i/labels" -X POST -d '{"labels":["status/ready"]}'
      curl_forgejo "repos/$FORGEJO_REPO/issues/$i/labels/status/wip" -X DELETE
    done

Self-throttle rules

  1. 3 итерации подряд idlesleep = min(current * 1.5, 60m)
  2. 3 итерации подряд successsleep = max(current * 0.8, 5m)
  3. 24h ничего не закрыл → result: idle 24h, эскалация (label needs-human)

Error escalation

Ошибка Действие
HTTP 401/403 от Forgejo PAT истёк / отозван → result: AUTH_ERROR, остановка окна
HTTP 500 от Forgejo result: forgejo down, sleep 30m
Subagent error 3× на одной issue +status/blocked +needs-human, отпустить assignee, next issue