--- name: _autonomous_pickup description: "[SHARED SNIPPET — do not invoke directly] Forgejo queue pickup logic, импортируется во все auto-*.md агенты. Содержит claim/state-transition contract." status: draft created_at: 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` / файлов. Проверь что выставлены: ```powershell # 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 окно) Замени `` на свою роль (`ANALYST`/`BACKEND`/`FRONTEND`/`REVIEWER`/`QA` — UPPER-case): ```powershell $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**: ```powershell 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) ```powershell # 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) ```bash 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) ```bash # Проверка через 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 ```bash 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` — иначе откатываемся. ```bash 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/ready` → `status/wip` | auto-backend / auto-frontend | claim успешный | | `status/wip` → `status/review` | worker | PR открыт | | `status/review` → `status/qa` | auto-code-reviewer | APPROVE + merge | | `status/review` → `status/blocked` | auto-code-reviewer | BLOCK/FIX verdict | | `status/qa` → `status/done` | auto-qa-tester | smoke OK, issue closed | | `status/qa` → `status/blocked` | auto-qa-tester | smoke FAIL | | `status/blocked` → `status/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. До имплементации — мониторь вручную. ```bash # Каждые 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 итерации подряд idle** → `sleep = min(current * 1.5, 60m)` 2. **3 итерации подряд success** → `sleep = 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 |