gendesign/.claude/agents/_autonomous_pickup.md
bot-analyst ae820d3bd9 docs(agents): auto-analyst verify-before-ready + dedup/loop guardrails
Из инцидентов live-сессии 2026-05-30:
- шаг 4: file:line из vault-заметки НЕВЕРИФИЦИРОВАН — Read+confirm своими глазами
  до вписывания в issue (incident: спека «перевести на JSON», код уже на JSON)
- шаг 7: status/ready только на финальном verified-теле, не «ready→переписываю»
  (incident #697/#699 — воркер смержил по тонкому телу за ~30s)
- «поменяй лейблы» ⇒ также аудит+переписывание тонкого тела до ready
- _autonomous_pickup: dedup-before-create обязателен при параллельных окнах
  (incident #724/#728 vs #726/#727), list_repo_issues всегда с фильтром (token-limit)
- work-as-analyst: один loop-механизм, CronDelete перед CronCreate
- fix stale cross-ref шаг 6 → шаг 8
2026-05-30 21:15:48 +03:00

18 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 операции — mcp__forgejo__* tools (PRIMARY)

forgejo MCP (goern) подключён и eager в bot-окнах. Токен бота — из FORGEJO_ACCESS_TOKEN (его выставляет scripts/start-bot.ps1 <role> ДО запуска 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: без фильтра ответ рвёт лимит (видели 59k140k символов → дамп в файл, тратятся тики на 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

⚠️ Таблица — снимок; при любом сомнении/ошибке резолвь 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) → 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/needs-fix auto-code-reviewer 🟠 FIX verdict (assignee остаётся worker)
status/needs-fixstatus/review original worker fixup-commit запушен в тот же PR
status/review/status/needs-fixstatus/blocked auto-code-reviewer 🔴 BLOCK (security/data-loss) ИЛИ 3× fix-fail
status/qastatus/done auto-qa-tester smoke OK, issue closed
status/qastatus/needs-fix auto-qa-tester smoke FAIL = feature_regression (assignee → PR author)
status/qastatus/blocked auto-qa-tester prod_down (+ pause-bots)
status/blockedstatus/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 снимает).

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/wipstatus/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, когда не работаешь.

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