- code-reviewer: self-merge policy 2026-06-27 vs ложный BLOCK; origin/gh → forgejo/MCP - phase-5-verdict: self-extending guard в 'когда НЕ мержить' - database-expert: prod DDL только через data/sql + deploy, не execute_sql - auto-*: model-текст и интервалы тиков синхронизированы с фактом - qa-tester: chrome-devtools → playwright; devops: Forgejo Actions
23 KiB
| 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'ы хранятся в двух местах одновременно:
- Vault
meta/00_credentials.md— sensitive backup (read-only reference) - Windows User-scope env vars — production-ready, persistent:
FORGEJO_TOKEN_ANALYSTFORGEJO_TOKEN_BACKENDFORGEJO_TOKEN_FRONTENDFORGEJO_TOKEN_REVIEWERFORGEJO_TOKEN_QAFORGEJO_URL_BOTS=https://git.gendsgn.ruFORGEJO_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 операции — mcp__forgejo__* tools (PRIMARY)
forgejo MCP (goern) подключён, но deferred (alwaysLoad:false во всех .claude/mcp/<role>.json —
экономия контекста, ~90 схем не грузятся upfront). Токен бота — из FORGEJO_ACCESS_TOKEN
(его выставляет scripts/start-bot.ps1 <role> ДО запуска claude).
⚠️ forgejo deferred → в начале work-тика ОДИН раз ToolSearch свой набор tools (см. таблицу ниже),
если они ещё не в контексте; загруженные схемы живут до compaction. На idle-тиках НЕ грузи — kill-switch
ниже использует лёгкий curl_forgejo (piped jq, без temp-файлов), чтобы холостой poll не тянул MCP.
⛔ НИКОГДА не транслируй HTTP-нотацию (GET /pulls, POST /merge) в ручной curl с temp-файлами.
Анти-паттерн (incident 2026-05-31, PR #893): curl ... -o /tmp/pr.json → python3 json.load(open(...))
= на Windows http=404 + FileNotFoundError /tmp/.... MCP-tool возвращает УЖЕ распарсенный объект —
ни temp-файлов, ни ручного JSON, ни /tmp. curl_forgejo ниже — fallback only (MCP недоступен,
напр. Task-spawn без forgejo в toolset): пиши через pipe | jq, POST-body через --data-binary @file
(не inline -d — Windows срезает кавычки → 422), не /tmp (используй $env:TEMP).
| Операция | 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=<keywords>&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-<role>author за последние минуты). - Дедуп-before-create ОБЯЗАТЕЛЕН (не опционален):
list_repo_issuesсq=<keywords>&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 недоступен)
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
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:
# Перед обычным 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-<scope>.md):
# 1. CONTEXT LOAD (как при обычной работе — conventions обязательны)
# 2. checkout СУЩЕСТВУЮЩЕЙ ветки feat/<N>-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:
- НЕ abort — finish текущий commit + push (минимизирует потерю работы)
- Open PR как обычно → PR попадёт в queue
status/review(но reviewer тоже paused → PR не merge'нётся) - result: PR #N opened, then paused due to kill-switch
- После 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.
- Idle → спи на штатном коротком интервале роли (reviewer ~2m · qa ~5m · worker ≤5m · analyst ~15m), БЕЗ прогрессивного роста. Хэндофы должны быть near-real-time.
- 24h ничего не закрыл → result: idle 24h, эскалация (label
needs-human). - Реальный потолок — 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; reviewer — opus). 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 |