gendesign/.claude/agents/_autonomous_pickup.md
lekss361 297eb29d65 feat(claude): env-vars refactor + stale-claim cron (Phase 2 finalization)
#11: slash-commands и _autonomous_pickup.md теперь читают persistent User-scope
env vars напрямую (FORGEJO_TOKEN_<ROLE>) вместо file-based из ~/.claude/secrets/.

Один раз: scripts/setup-bot-env.ps1 (запускается user'ом локально, кладёт
токены из vault в Windows User registry). После этого окна grab credentials
через [System.Environment]::GetEnvironmentVariable() — никаких файлов.

Все 5 work-as-*.md обновлены (analyst/backend/frontend/reviewer/qa).

#12: stale-claim cleanup — cron каждые 30 минут.

- scripts/cleanup-stale-claims.sh — bash, parses Forgejo API + Python для JSON
- .forgejo/workflows/stale-claims.yml — cron schedule `*/30 * * * *`

Освобождает issues застрявшие в status/wip >4h:
  - assignees=[]
  - +status/ready -status/wip
  - comment "stale claim released, worker likely crashed"

Использует secret FORGEJO_BOT_QA_TOKEN (минимум прав = write:issue).

Setup: secret нужно завести в Forgejo Actions UI после merge —
git.gendsgn.ru/lekss361/gendesign/-/settings/actions/secrets.

Refs: PRs #608, #609. Closes TODO с PR #608 (stale-claim cron).
2026-05-28 00:24:45 +03:00

225 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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
[System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User") | Out-Null
if (-not $?) { Write-Error "❌ Запусти 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
```
## 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 |