--- 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 операции — `mcp__forgejo__*` tools (PRIMARY) forgejo MCP (goern) подключён и eager в bot-окнах. Токен бота — из `FORGEJO_ACCESS_TOKEN` (его выставляет `scripts/start-bot.ps1 ` ДО запуска claude). **Используй MCP-tools, не curl.** Псевдо-bash `curl_forgejo` ниже — **fallback**, только если MCP недоступен. | Операция | MCP tool | Заметки | |---|---|---| | kill-switch / pickup / fixup-pickup | `mcp__forgejo__list_repo_issues` | фильтр `labels`,`state`; unassigned/assignee — фильтруй клиентом (`.assignees`) | | claim: assign + label transition | `mcp__forgejo__update_issue` (assignees) + `mcp__forgejo__add_issue_labels` + `mcp__forgejo__remove_issue_labels` | | | PR open | `mcp__forgejo__create_pull_request` | head/base/title/body | | PR diff / files | `mcp__forgejo__get_pull_request_diff`, `mcp__forgejo__list_pull_request_files` | diff умеет `file_path` | | review verdict | `mcp__forgejo__create_pull_review` | event=APPROVED / REQUEST_CHANGES / COMMENT | | merge | `mcp__forgejo__merge_pull_request` | Do=squash, delete_branch_after_merge | | comment (marker / fixup K/3) | `mcp__forgejo__create_issue_comment` | | | status transition | `mcp__forgejo__add_issue_labels` / `mcp__forgejo__remove_issue_labels` | | | close issue (qa done) | `mcp__forgejo__issue_state_change` | | **Gotcha:** на user-репо (`lekss361` — не org) для label-листинга передавай `include_org_labels:false`, иначе 403 на `/orgs/...`. **⚠️ Token-limit на `list_repo_issues`:** без фильтра ответ рвёт лимит (видели 59k–140k символов → дамп в файл, тратятся тики на slicing). ВСЕГДА передавай `labels`+`state`+узкий `limit` (напр. `labels:"status/ready"`, `limit:30`). Не звать без фильтра «посмотреть все issues» — для дедупа используй `q=&state=all&limit=5`, не полный листинг. **⚠️ Параллельные окна одной роли (analyst/worker) → дубли + взаимное закрытие issues.** Два окна на одном токене не имеют claim-lock на *создание* issue. Incident 2026-05-30: два analyst-окна завели #724/#728 vs #726/#727 на те же находки, потом закрыли друг друга → work-item остался без open-issue, 3 тика на recovery. Защита: - **Перед /loop**: убедись, что нет второго live-окна твоей роли (спроси человека / проверь recent issues на свой `bot-` author за последние минуты). - **Дедуп-before-create ОБЯЗАТЕЛЕН** (не опционален): `list_repo_issues` с `q=&state=all` ПЕРЕД каждым `create_issue`. Совпадение по сути → не создавай, прокомментируй существующий. - Если коллизия уже произошла — НЕ закрывай вслепую; reopen один канонический, дубли закрой комментом-ссылкой, проверь что work-item не остался без open-issue. ### Label IDs — goern `add_issue_labels`/`remove_issue_labels` требует ID, НЕ имя! goern-MCP в add/remove принимает **числовой id** (несмотря на доку «names» — первый add по имени упадёт). **Перед add/remove**: либо id из таблицы ниже, либо (надёжнее — id меняются при пересоздании label) `mcp__forgejo__list_repo_labels` → построй map name→id рантайм. Pipeline-labels (snapshot 2026-05-30): | label | id | label | id | |---|---|---|---| | `scope/backend` | 46 | `status/ready` | 51 | | `scope/frontend` | 47 | `status/wip` | 52 | | `scope/db` | 48 | `status/review` | 53 | | `scope/qa` | 49 | `status/qa` | 54 | | `scope/devops` | 50 | `status/done` | 55 | | `priority/p0` | 57 | `status/blocked` | 56 | | `priority/p1` | 58 | `status/needs-fix` | 62 | | `priority/p2` | 59 | `pause-bots` | 60 | | `priority/p3` | 63 | `needs-human` | 61 | | `bug` | 5 | `tech-debt` | 42 | | `status/needs-analysis` | 64 | | | ⚠️ Таблица — снимок; при любом сомнении/ошибке резолвь id через `list_repo_labels` (источник истины). `create_issue` принимает label-ids массивом; `issue_state_change` — для open/close (не labels). ## Forgejo API endpoints — curl FALLBACK (если MCP недоступен) ```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 ``` ## Fixup pickup — own `status/needs-fix` PR (priority над new claim) Reviewer НЕ дед-эндит 🟠 FIX в human (это был главный throughput-killer). FIX verdict → issue получает `status/needs-fix`, assignee **остаётся** worker'а. Worker КАЖДЫЙ work-тик ПЕРВЫМ делом проверяет свои `needs-fix` (приоритет над новым claim) и чинит свой же PR — НЕ создаёт новый branch/PR: ```bash # Перед обычным ready-pickup — есть ли мой PR, который вернули на фикс? MINE_FIX=$(curl_forgejo \ "repos/$FORGEJO_REPO/issues?state=open&labels=scope/$SCOPE,status/needs-fix&sort=oldest&limit=20" \ | jq -r --arg me "$BOT_USERNAME" '[.[] | select(.assignees[]?.login == $me)][0].number // ""') if [[ -n "$MINE_FIX" ]]; then # FIXUP MODE (детальный flow — в auto-.md): # 1. CONTEXT LOAD (как при обычной работе — conventions обязательны) # 2. checkout СУЩЕСТВУЮЩЕЙ ветки feat/-slug (git fetch forgejo-bot && checkout) # 3. прочитать последний review-bot comment (marker verdict=changes) → fix-list # 4. применить фиксы → lint → tests → push в ТОТ ЖЕ branch (PR обновится) # 5. issue: +status/review -status/needs-fix exit 0 fi # иначе — обычный ready-pickup ниже ``` **Fix-attempt cap**: каждый fixup-цикл добавляет comment `fixup attempt K/3`. На 3-м FIX по одному PR reviewer переводит в `+status/blocked +needs-human` (защита от бесконечного fix-loop). ## State transitions reference | От → К | Кто переключает | Условие | |---|---|---| | (new, human) → `status/needs-analysis` | **человек** (или auto-analyst для своих raw-находок) | сырой/нечёткий тикет заведён в Forgejo, требует archeology+декомпозиции до того как worker сможет взять | | `status/needs-analysis` → `status/ready` | **auto-analyst** (claim+refine in-place) | тикет single-scope, переписан в actionable-спек по шаблону, deps удовлетворены | | `status/needs-analysis` → (closed, links на под-issues) | **auto-analyst** | тикет был multi-scope → расщеплён на N под-issues (scope/*+ready), parent закрыт коммент-ссылкой | | `status/needs-analysis` → `+needs-human` | auto-analyst | неустранимая двусмысленность / нужно решение/caps человека | | (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/needs-fix` | auto-code-reviewer | 🟠 FIX verdict (assignee остаётся worker) | | `status/needs-fix` → `status/review` | original worker | fixup-commit запушен в тот же PR | | `status/review`/`status/needs-fix` → `status/blocked` | auto-code-reviewer | 🔴 BLOCK (security/data-loss) ИЛИ 3× fix-fail | | `status/qa` → `status/done` | auto-qa-tester | smoke OK, issue closed | | `status/qa` → `status/needs-fix` | auto-qa-tester | smoke FAIL = feature_regression (assignee → PR author) | | `status/qa` → `status/blocked` | auto-qa-tester | prod_down (+ pause-bots) | | `status/blocked` → `status/ready` | human ИЛИ **auto-resolver** | manual / human-proxy unblock | | `+needs-human` (вешать) | auto-analyst / worker / qa | блокер требует caps/решения человека | | `-needs-human` (снимать) → FSM | **only auto-resolver** (human-proxy окно) | блокер устранён; аналитику/воркерам снимать ЗАПРЕЩЕНО (anti-race #726/#727) | | любой + `pause-bots` присутствует | (никто не работает) | kill-switch | > **Новый label `status/needs-fix`** нужно создать в Forgejo (Settings → Labels) до первого запуска > auto-fix loop. Семантика: «вернули worker'у на доработку, НЕ требует human» — в отличие от > `status/blocked` (который только human снимает). > > **Label `status/needs-analysis` (id 64) — создан 2026-05-31.** Семантика: «человек завёл сырой > тикет, нужна archeology + декомпозиция аналитиком до того как worker возьмёт». Это **входящая > очередь auto-analyst** — единственный потребитель. Человек просто заводит issue с этим лейблом > (тело может быть нечётким — аналитик дочистит); scope/* и priority/* опциональны (аналитик > проставит). См. «Inbound pickup» в `auto-analyst.md`. ## 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 — ✅ имплементировано (cron) Освобождение issues застрявших в `status/wip` >4h автоматизировано: - **Workflow**: `.forgejo/workflows/stale-claims.yml` — cron `*/30 * * * *` (каждые 30 мин UTC) - **Скрипт**: `scripts/cleanup-stale-claims.sh` (`STALE_HOURS=4`, пагинация, trace-comment на каждый release) - **Действие**: clear assignee → `status/wip` → `status/ready` + comment "Stale claim released…" Ручной мониторинг wip-issues больше **не нужен**. Manual trigger возможен через Forgejo UI (`workflow_dispatch`). > ⚠️ **Известный gap**: cron НЕ проверяет `pause-bots`. Если worker приостановлен mid-work > (держит wip-claim до merge per «Pause-bots поведение») и завис >4h — cron всё равно снимет > claim. Добавить early-exit по `pause-bots` в `cleanup-stale-claims.sh` (follow-up). ## Self-throttle rules > **Подписка, не API → лупы ТУГИЕ, без cost-backoff.** Idle-тик = дешёвый poll (реальный usage > тратится только когда есть work). Прогрессивный backoff был ради экономии API-стоимости — на > подписке этой причины нет, а он лишь тормозил хэндофы (ready→wip→review→qa) до 30-60m. 1. **Idle** → спи на штатном коротком интервале роли (reviewer ~2m · qa ~5m · worker ≤5m · analyst ~15m), БЕЗ прогрессивного роста. Хэндофы должны быть near-real-time. 2. **24h ничего не закрыл** → result: idle 24h, эскалация (label `needs-human`). 3. **Реальный потолок — usage-лимиты подписки** (Max 5h/weekly), не деньги-за-тик. Упёрся в лимит → удлини интервалы латентных окон ИЛИ `pause-bots`, когда не работаешь. ### Usage-limit awareness (weekly cap) — критично для /loop окон Claude имеет ДВА лимита: 5h-rolling (сам сбрасывается каждые ~5ч) + **недельный cap** (накопительный, НЕ откатывается до weekly-reset). Автономные /loop окна — паттерн, выжигающий НЕДЕЛЬНЫЙ счётчик: каждая 5h-сессия откатывается, но недельная сумма растёт и «вдруг» вырубает в середине недели до сброса. **Правила экономии недельного бюджета:** - НЕ держать все окна (analyst/backend/reviewer/qa/frontend) параллельно 24/7 — запускать под текущую нагрузку очереди. - **Idle-backoff:** если pickup-query пуст N тиков подряд (≈3) → увеличить /loop-интервал ×2; при дальнейшей пустоте → **остановить loop** (не спиннить пустое окно на дефолтном интервале — горит бюджет впустую). Перезапустить, когда появится работа. - Пустая очередь + нет fixup-PR → выходить из loop, а не крутиться вхолостую. - Тяжёлые batch-прогоны — вне пиковых часов (≈5–11 PT) при возможности. - Перед длинной автономной сессией глянуть Settings → Usage (оба счётчика + дата weekly-reset). - **Окна — на Sonnet, не Opus** (`start-bot.ps1` уже запускает с `--model sonnet`). Opus — только main-оркестратору. Sonnet-пул отдельный, недельный All-models (Opus) пул так не горит. - **Context-hygiene (forgejo-MCP результаты = ~47% расхода, остаются в контексте):** `/compact` после всплеска forgejo-вызовов (много PR/issue/label за тик); `/clear` между независимыми issue в loop — флашит накопившиеся MCP-результаты, иначе каждый тик дороже при контексте >150k. ## 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 |