Fix structural causes of autonomous-window underperformance vs single-window orchestration — persona prompts were thin pointers that deferred conventions to unread files and dead-ended every imperfect PR into human review. - Inline-load conventions: каждый work-тик MANDATORY-читает полный backend/frontend-engineer.md + rules (был optional pointer, который cost-rule отучал читать). Cost-saving "не строй контекст" сужен до idle-тиков явно. - Auto-fix loop: reviewer FIX -> status/needs-fix (assignee остаётся worker), worker сам подхватывает свой PR и пушит fixup в ту же ветку. blocked+needs-human зарезервирован под BLOCK (security/data-loss) или 3x fix-fail (+ fix-attempt cap). QA feature_regression -> needs-fix аналогично. FSM-таблица обновлена. - Fat issue template: auto-analyst делает mandatory code archeology (Grep/Read) и пишет Files/сигнатуры/Acceptance/Risk (формат tech-analyst) — issue это единственный канал к worker'у. - Model resolution doc: model: frontmatter мёртв в standalone /loop-окне (модель = модель окна); reviewer-окно запускать в Opus 4.8 осознанно. - Drift fix: stale-claim cleanup помечен implemented (cron stale-claims.yml в проде, #610-612); отмечен gap pause-bots vs cleanup. Новый label status/needs-fix создать в Forgejo до первого запуска auto-fix loop.
261 lines
12 KiB
Markdown
261 lines
12 KiB
Markdown
---
|
||
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 окно)
|
||
|
||
Замени `<ROLE>` на свою роль (`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
|
||
```
|
||
|
||
## 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-<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/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` | **only human** | manual unblock |
|
||
| любой + `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/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
|
||
|
||
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 |
|