feat(claude): env-vars refactor + stale-claim cron (Phase 2 final) #610
9 changed files with 398 additions and 70 deletions
|
|
@ -15,42 +15,53 @@ created_at: 2026-05-27
|
||||||
> **Это критично.** Без правильной настройки git identity → commits будут писаться
|
> **Это критично.** Без правильной настройки git identity → commits будут писаться
|
||||||
> под user'ом (lekss361), не под ботом. Audit trail сломается.
|
> под user'ом (lekss361), не под ботом. Audit trail сломается.
|
||||||
|
|
||||||
### Шаг 1 — Получить PAT из vault
|
### Шаг 1 — Где живут PAT'ы
|
||||||
|
|
||||||
PAT'ы для всех 5 ботов хранятся в **`meta/00_credentials.md`** (vault, sensitive,
|
PAT'ы хранятся в **двух местах одновременно**:
|
||||||
не в git). Один раз выгрузи их в локальные secrets:
|
|
||||||
|
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
|
```powershell
|
||||||
# Локация — никогда не коммитится (gitignored через ~/.gitignore_global)
|
# GetEnvironmentVariable возвращает $null если var отсутствует — НЕ throws.
|
||||||
New-Item -ItemType Directory -Force ~/.claude/secrets | Out-Null
|
# Поэтому проверяем результат напрямую (не через $? — он у Get* всегда $true).
|
||||||
|
if (-not [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User")) {
|
||||||
# Для каждого бота — скопируй PAT из vault в файл (открой meta/00_credentials.md,
|
Write-Error "❌ FORGEJO_TOKEN_BACKEND не выставлен. Запусти scripts/setup-bot-env.ps1 сначала"
|
||||||
# секция "Forgejo bot-users", найди PAT для bot-<role>, вставь сюда):
|
}
|
||||||
# ~/.claude/secrets/forgejo-bot-analyst.token
|
|
||||||
# ~/.claude/secrets/forgejo-bot-backend.token
|
|
||||||
# ~/.claude/secrets/forgejo-bot-frontend.token
|
|
||||||
# ~/.claude/secrets/forgejo-bot-reviewer.token
|
|
||||||
# ~/.claude/secrets/forgejo-bot-qa.token
|
|
||||||
#
|
|
||||||
# Пример (с placeholder — НЕ commit'ить настоящий token):
|
|
||||||
# "<PAT-FROM-VAULT>" | Set-Content ~/.claude/secrets/forgejo-bot-<role>.token -NoNewline
|
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ Проверь что `~/.claude/secrets/` есть в `~/.gitignore_global` (или эквивалентном).
|
|
||||||
|
|
||||||
### Шаг 2 — Env vars + git identity (per окно)
|
### Шаг 2 — Env vars + git identity (per окно)
|
||||||
|
|
||||||
Замени `<role>` на свою роль (`analyst`/`backend`/`frontend`/`reviewer`/`qa`):
|
Замени `<ROLE>` на свою роль (`ANALYST`/`BACKEND`/`FRONTEND`/`REVIEWER`/`QA` — UPPER-case):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$ROLE = "backend" # ← ИЗМЕНИ ПЕРЕД ЗАПУСКОМ
|
$ROLE = "BACKEND" # ← ИЗМЕНИ ПЕРЕД ЗАПУСКОМ (UPPER-case)
|
||||||
$BOT = "bot-$ROLE"
|
$BOT = "bot-$($ROLE.ToLower())"
|
||||||
|
|
||||||
# Forgejo API credentials
|
# Resolve token из persistent User env
|
||||||
$env:FORGEJO_TOKEN = (Get-Content ~/.claude/secrets/forgejo-$BOT.token -Raw).Trim()
|
$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_$ROLE", "User")
|
||||||
$env:BOT_USERNAME = $BOT
|
$env:BOT_USERNAME = $BOT
|
||||||
$env:FORGEJO_URL = "https://git.gendsgn.ru"
|
$env:FORGEJO_URL = [System.Environment]::GetEnvironmentVariable("FORGEJO_URL_BOTS", "User")
|
||||||
$env:FORGEJO_REPO = "lekss361/gendesign"
|
$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)
|
# Git identity — КРИТИЧНО, иначе commit author будет user'а (lekss361)
|
||||||
$env:GIT_AUTHOR_NAME = $BOT
|
$env:GIT_AUTHOR_NAME = $BOT
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,28 @@ description: Запустить окно как auto-analyst (декомпози
|
||||||
|
|
||||||
## Pre-flight checks (выполни СЕЙЧАС, до /loop)
|
## Pre-flight checks (выполни СЕЙЧАС, до /loop)
|
||||||
|
|
||||||
1. **Verify env vars set:**
|
```powershell
|
||||||
- `$env:FORGEJO_TOKEN` (для bot-analyst — см. `meta/00_credentials.md`)
|
# 1. Resolve credentials из persistent User env
|
||||||
- `$env:BOT_USERNAME = "bot-analyst"`
|
$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_ANALYST", "User")
|
||||||
- `$env:FORGEJO_URL = "https://git.gendsgn.ru"`
|
$env:BOT_USERNAME = "bot-analyst"
|
||||||
- `$env:FORGEJO_REPO = "lekss361/gendesign"`
|
$env:FORGEJO_URL = [System.Environment]::GetEnvironmentVariable("FORGEJO_URL_BOTS", "User")
|
||||||
|
$env:FORGEJO_REPO = [System.Environment]::GetEnvironmentVariable("FORGEJO_REPO_BOTS", "User")
|
||||||
|
|
||||||
2. **Verify identity:**
|
if (-not $env:FORGEJO_TOKEN) {
|
||||||
```bash
|
Write-Error "❌ FORGEJO_TOKEN_ANALYST не выставлен. Запусти scripts/setup-bot-env.ps1"
|
||||||
curl -sH "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_URL/api/v1/user" | jq .login
|
return
|
||||||
# Должен вернуть "bot-analyst"
|
}
|
||||||
```
|
|
||||||
|
|
||||||
3. **Если pre-flight FAILS** — НЕ запускай /loop. Сообщи user'у что нужно настроить.
|
# 2. Verify identity (analyst делает только curl issues, git identity не нужен здесь)
|
||||||
|
$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"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Write-Host "✓ PAT belongs to $($me.login) — analyst persona ready"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Если pre-flight FAILS** — НЕ запускай /loop. Обычно — env vars не выставлены, запусти `setup-bot-env.ps1`.
|
||||||
|
|
||||||
## Behavior contract
|
## Behavior contract
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,35 +9,40 @@ description: Запустить окно как auto-backend (pickup scope/backe
|
||||||
|
|
||||||
## Pre-flight checks (выполни СЕЙЧАС, до /loop)
|
## Pre-flight checks (выполни СЕЙЧАС, до /loop)
|
||||||
|
|
||||||
1. **Env vars (см. `.claude/agents/_autonomous_pickup.md` Шаги 1-2):**
|
```powershell
|
||||||
```powershell
|
# 1. Resolve credentials из persistent User env (выставлены setup-bot-env.ps1)
|
||||||
$env:FORGEJO_TOKEN = Get-Content ~/.claude/secrets/forgejo-bot-backend.token -Raw
|
$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User")
|
||||||
$env:BOT_USERNAME = "bot-backend"
|
$env:BOT_USERNAME = "bot-backend"
|
||||||
$env:FORGEJO_URL = "https://git.gendsgn.ru"
|
$env:FORGEJO_URL = [System.Environment]::GetEnvironmentVariable("FORGEJO_URL_BOTS", "User")
|
||||||
$env:FORGEJO_REPO = "lekss361/gendesign"
|
$env:FORGEJO_REPO = [System.Environment]::GetEnvironmentVariable("FORGEJO_REPO_BOTS", "User")
|
||||||
```
|
|
||||||
|
|
||||||
2. **Git identity (КРИТИЧНО — иначе commits под lekss361):**
|
if (-not $env:FORGEJO_TOKEN) {
|
||||||
```powershell
|
Write-Error "❌ FORGEJO_TOKEN_BACKEND не выставлен. Запусти scripts/setup-bot-env.ps1"
|
||||||
$env:GIT_AUTHOR_NAME = $env:BOT_USERNAME
|
return
|
||||||
$env:GIT_AUTHOR_EMAIL = "$($env:BOT_USERNAME)@gendsgn.local"
|
}
|
||||||
$env:GIT_COMMITTER_NAME = $env:GIT_AUTHOR_NAME
|
|
||||||
$env:GIT_COMMITTER_EMAIL = $env:GIT_AUTHOR_EMAIL
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Bot-remote для push (audit-log под bot):**
|
# 2. Git identity — КРИТИЧНО, иначе commits под lekss361
|
||||||
```powershell
|
$env:GIT_AUTHOR_NAME = $env:BOT_USERNAME
|
||||||
git remote remove forgejo-bot 2>$null
|
$env:GIT_AUTHOR_EMAIL = "$($env:BOT_USERNAME)@gendsgn.local"
|
||||||
git remote add forgejo-bot "https://$($env:BOT_USERNAME):$($env:FORGEJO_TOKEN)@git.gendsgn.ru/lekss361/gendesign.git"
|
$env:GIT_COMMITTER_NAME = $env:GIT_AUTHOR_NAME
|
||||||
```
|
$env:GIT_COMMITTER_EMAIL = $env:GIT_AUTHOR_EMAIL
|
||||||
|
|
||||||
4. **Verify identity:**
|
# 3. Bot-remote для push (audit log под bot, не lekss361)
|
||||||
```bash
|
git remote remove forgejo-bot 2>$null
|
||||||
curl -sH "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_URL/api/v1/user" | jq .login
|
git remote add forgejo-bot "https://$($env:BOT_USERNAME):$($env:FORGEJO_TOKEN)@git.gendsgn.ru/lekss361/gendesign.git"
|
||||||
# Должен вернуть "bot-backend"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Если что-то FAILS** — НЕ запускай /loop. Скажи user'у что не так.
|
# 4. Verify identity
|
||||||
|
$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"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Write-Host "✓ PAT belongs to $($me.login)"
|
||||||
|
Write-Host "✓ Commits authored as: $env:GIT_AUTHOR_NAME <$env:GIT_AUTHOR_EMAIL>"
|
||||||
|
Write-Host "✓ Use 'git push forgejo-bot' для push (НЕ forgejo — он lekss361)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Если что-то FAILS** — НЕ запускай /loop. Скажи user'у (обычно — env vars не выставлены, запусти `setup-bot-env.ps1`).
|
||||||
|
|
||||||
## Behavior contract
|
## Behavior contract
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,15 @@ description: Запустить окно как auto-frontend (pickup scope/fron
|
||||||
|
|
||||||
## Pre-flight checks
|
## Pre-flight checks
|
||||||
|
|
||||||
См. `.claude/commands/work-as-backend.md` для полного pre-flight — идентично, только:
|
Идентично `work-as-backend.md`, только изменить две строки:
|
||||||
- `BOT_USERNAME = "bot-frontend"`
|
|
||||||
- PAT из `~/.claude/secrets/forgejo-bot-frontend.token`
|
```powershell
|
||||||
|
$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_FRONTEND", "User")
|
||||||
|
$env:BOT_USERNAME = "bot-frontend"
|
||||||
|
# остальные строки (FORGEJO_URL/REPO, git identity, bot-remote, verify) — без изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
См. полный pre-flight в `.claude/commands/work-as-backend.md`.
|
||||||
|
|
||||||
## Behavior contract
|
## Behavior contract
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,13 @@ description: Запустить окно как auto-qa-tester (Playwright smoke
|
||||||
|
|
||||||
## Pre-flight checks
|
## Pre-flight checks
|
||||||
|
|
||||||
Идентично `work-as-backend.md`, только:
|
Идентично `work-as-backend.md`, только изменить две строки:
|
||||||
- `BOT_USERNAME = "bot-qa"`
|
|
||||||
- PAT из `~/.claude/secrets/forgejo-bot-qa.token`
|
```powershell
|
||||||
|
$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_QA", "User")
|
||||||
|
$env:BOT_USERNAME = "bot-qa"
|
||||||
|
# остальное — см. work-as-backend.md
|
||||||
|
```
|
||||||
|
|
||||||
**Дополнительно** — этот bot использует Playwright MCP, проверь что доступен:
|
**Дополнительно** — этот bot использует Playwright MCP, проверь что доступен:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,13 @@ description: Запустить окно как auto-code-reviewer (review + mer
|
||||||
|
|
||||||
## Pre-flight checks
|
## Pre-flight checks
|
||||||
|
|
||||||
Идентично `work-as-backend.md`, только:
|
Идентично `work-as-backend.md`, только изменить две строки:
|
||||||
- `BOT_USERNAME = "bot-reviewer"`
|
|
||||||
- PAT из `~/.claude/secrets/forgejo-bot-reviewer.token`
|
```powershell
|
||||||
|
$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_REVIEWER", "User")
|
||||||
|
$env:BOT_USERNAME = "bot-reviewer"
|
||||||
|
# остальное — см. work-as-backend.md
|
||||||
|
```
|
||||||
|
|
||||||
**Дополнительно** — этот bot имеет merge authority, поэтому verify scope более строго:
|
**Дополнительно** — этот bot имеет merge authority, поэтому verify scope более строго:
|
||||||
|
|
||||||
|
|
|
||||||
39
.forgejo/workflows/stale-claims.yml
Normal file
39
.forgejo/workflows/stale-claims.yml
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Cleanup stale claims for autonomous multi-agent workflow.
|
||||||
|
#
|
||||||
|
# Запускается каждые 30 минут (cron). Освобождает issues застрявшие в
|
||||||
|
# `status/wip` дольше 4 часов. См. `scripts/cleanup-stale-claims.sh`.
|
||||||
|
#
|
||||||
|
# Secrets:
|
||||||
|
# FORGEJO_BOT_QA_TOKEN — bot-qa PAT (write:issue scope нужен).
|
||||||
|
# Можно использовать любой bot-PAT с write:issue, но bot-qa имеет минимум прав.
|
||||||
|
|
||||||
|
name: cleanup-stale-claims
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Каждые 30 минут UTC
|
||||||
|
- cron: '*/30 * * * *'
|
||||||
|
workflow_dispatch: {} # manual run возможен через UI
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: stale-claims
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run cleanup script
|
||||||
|
env:
|
||||||
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_BOT_QA_TOKEN }}
|
||||||
|
FORGEJO_URL: https://git.gendsgn.ru
|
||||||
|
FORGEJO_REPO: lekss361/gendesign
|
||||||
|
STALE_HOURS: 4
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/cleanup-stale-claims.sh
|
||||||
|
./scripts/cleanup-stale-claims.sh
|
||||||
161
scripts/cleanup-stale-claims.sh
Normal file
161
scripts/cleanup-stale-claims.sh
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Cleanup stale claims for autonomous multi-agent workflow.
|
||||||
|
#
|
||||||
|
# Освобождает issues застрявшие в `status/wip` дольше 4 часов:
|
||||||
|
# - убирает assignee
|
||||||
|
# - меняет label status/wip → status/ready (delete wip → add ready)
|
||||||
|
#
|
||||||
|
# Why: если worker crashes или window закрывается во время работы, issue
|
||||||
|
# остаётся wip+assigned forever. Без cleanup queue забивается.
|
||||||
|
#
|
||||||
|
# Run: cron каждые 30 минут (см. .forgejo/workflows/stale-claims.yml).
|
||||||
|
#
|
||||||
|
# Env vars (требуются):
|
||||||
|
# FORGEJO_TOKEN — admin или с write:issue scope на repo
|
||||||
|
# FORGEJO_URL — https://git.gendsgn.ru (без /api/v1)
|
||||||
|
# FORGEJO_REPO — lekss361/gendesign
|
||||||
|
# STALE_HOURS — порог в часах (default 4)
|
||||||
|
#
|
||||||
|
# Assumption: issue.updated_at = последний touch (label/comment/assignee
|
||||||
|
# change). Worker делающий task >4h без касаний issue будет считаться stale.
|
||||||
|
# Для текущих small tasks 4h enough; в Phase 4 можно добавить heartbeat
|
||||||
|
# comment'ы от worker'а.
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — success (even if 0 stale found)
|
||||||
|
# 1 — env vars missing / API error
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${FORGEJO_TOKEN:?FORGEJO_TOKEN required}"
|
||||||
|
: "${FORGEJO_URL:?FORGEJO_URL required}"
|
||||||
|
: "${FORGEJO_REPO:?FORGEJO_REPO required}"
|
||||||
|
|
||||||
|
export STALE_HOURS="${STALE_HOURS:-4}"
|
||||||
|
STALE_SECS=$((STALE_HOURS * 3600))
|
||||||
|
|
||||||
|
curl_forgejo() {
|
||||||
|
curl -sS -H "Authorization: token $FORGEJO_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$FORGEJO_URL/api/v1/$1" "${@:2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
now_utc_epoch() {
|
||||||
|
python3 -c 'import datetime; print(int(datetime.datetime.now(datetime.timezone.utc).timestamp()))'
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[$(date -Is)] cleanup-stale-claims: scanning status/wip issues (stale threshold: ${STALE_HOURS}h)"
|
||||||
|
|
||||||
|
NOW=$(now_utc_epoch)
|
||||||
|
export CUTOFF=$((NOW - STALE_SECS))
|
||||||
|
|
||||||
|
# GET all open issues с label status/wip — pagination в Python (более надёжно чем
|
||||||
|
# склеивать JSON arrays в bash). Defensive cap 10 pages × 50 = 500.
|
||||||
|
|
||||||
|
ISSUES_TMP=$(mktemp)
|
||||||
|
trap 'rm -f "$ISSUES_TMP"' EXIT
|
||||||
|
|
||||||
|
for PAGE in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
PAGE_RESP=$(curl_forgejo "repos/$FORGEJO_REPO/issues?labels=status/wip&state=open&limit=50&page=$PAGE")
|
||||||
|
echo "$PAGE_RESP" >> "$ISSUES_TMP"
|
||||||
|
echo "---PAGE_END---" >> "$ISSUES_TMP"
|
||||||
|
PAGE_LEN=$(echo "$PAGE_RESP" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))')
|
||||||
|
if [ "$PAGE_LEN" -lt 50 ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Парсим JSON
|
||||||
|
cat "$ISSUES_TMP" | python3 <<'PYEOF'
|
||||||
|
import json, os, subprocess, sys, datetime
|
||||||
|
|
||||||
|
cutoff = int(os.environ.get("CUTOFF", "0"))
|
||||||
|
if cutoff == 0:
|
||||||
|
print("[ERROR] CUTOFF env var не выставлен или равен 0 — abort", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Read pagination output — multiple JSON arrays separated by "---PAGE_END---"
|
||||||
|
raw = sys.stdin.read()
|
||||||
|
pages = [p.strip() for p in raw.split("---PAGE_END---") if p.strip()]
|
||||||
|
data = []
|
||||||
|
for p in pages:
|
||||||
|
try:
|
||||||
|
data.extend(json.loads(p))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"[WARN] failed to parse page (len={len(p)}), skipping", file=sys.stderr)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
print("[OK] 0 wip issues — nothing to do")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
released = 0
|
||||||
|
errors = 0
|
||||||
|
now_epoch = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||||
|
|
||||||
|
for issue in data:
|
||||||
|
issue_num = issue["number"]
|
||||||
|
updated_at = issue["updated_at"]
|
||||||
|
title = issue["title"]
|
||||||
|
assignees = [a["login"] for a in (issue.get("assignees") or [])]
|
||||||
|
|
||||||
|
updated_epoch = int(datetime.datetime.fromisoformat(updated_at.replace("Z", "+00:00")).timestamp())
|
||||||
|
age_secs = now_epoch - updated_epoch
|
||||||
|
age_hours = age_secs // 3600
|
||||||
|
|
||||||
|
if updated_epoch >= cutoff:
|
||||||
|
print(f" · #{issue_num} '{title[:50]}' — age {age_hours}h, fresh, skip")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f" ⚠ #{issue_num} '{title[:50]}' — age {age_hours}h, STALE")
|
||||||
|
print(f" assignees: {assignees}")
|
||||||
|
print(f" → releasing claim")
|
||||||
|
|
||||||
|
repo = os.environ["FORGEJO_REPO"]
|
||||||
|
base = os.environ["FORGEJO_URL"] + "/api/v1"
|
||||||
|
token = os.environ["FORGEJO_TOKEN"]
|
||||||
|
auth = ["-H", f"Authorization: token {token}", "-H", "Content-Type: application/json"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Clear assignees
|
||||||
|
subprocess.run(
|
||||||
|
["curl", "-sS", "-X", "PATCH", *auth, "-d", '{"assignees": []}',
|
||||||
|
f"{base}/repos/{repo}/issues/{issue_num}"],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Remove status/wip label FIRST (если падает — issue в neutral state без status/*,
|
||||||
|
# что менее опасно чем оба status/ready+status/wip одновременно)
|
||||||
|
subprocess.run(
|
||||||
|
["curl", "-sS", "-X", "DELETE", *auth,
|
||||||
|
f"{base}/repos/{repo}/issues/{issue_num}/labels/status%2Fwip"],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Add status/ready label
|
||||||
|
subprocess.run(
|
||||||
|
["curl", "-sS", "-X", "POST", *auth, "-d", '{"labels": ["status/ready"]}',
|
||||||
|
f"{base}/repos/{repo}/issues/{issue_num}/labels"],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Post comment for trace
|
||||||
|
comment_body = json.dumps({
|
||||||
|
"body": f"⚠️ Stale claim released: issue stuck in status/wip for {age_hours}h "
|
||||||
|
f"(previously assigned to {', '.join(assignees) or 'none'}). "
|
||||||
|
f"Label transition: status/wip → status/ready. Worker likely crashed."
|
||||||
|
})
|
||||||
|
subprocess.run(
|
||||||
|
["curl", "-sS", "-X", "POST", *auth, "-d", comment_body,
|
||||||
|
f"{base}/repos/{repo}/issues/{issue_num}/comments"],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
released += 1
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f" ✗ FAILED: {e.stderr.decode() if e.stderr else 'unknown'}")
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"[OK] Released {released} stale claim(s), {errors} errors")
|
||||||
|
sys.exit(1 if errors else 0)
|
||||||
|
PYEOF
|
||||||
89
scripts/setup-bot-env.ps1
Normal file
89
scripts/setup-bot-env.ps1
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
# setup-bot-env.ps1
|
||||||
|
# Set Windows User-scope env vars для autonomous multi-agent workflow.
|
||||||
|
#
|
||||||
|
# Запускается ОДИН РАЗ на каждой машине пользователя. Не нужен admin.
|
||||||
|
# Effect: 7 User-scope env vars persistent в registry.
|
||||||
|
#
|
||||||
|
# Где взять токены:
|
||||||
|
# 1. Vault meta/00_credentials.md (sensitive, не в git) — секция "Forgejo bot-users"
|
||||||
|
# 2. Или сгенерировать новые: см. там же секция "Rotation / revocation"
|
||||||
|
#
|
||||||
|
# Безопасность:
|
||||||
|
# - Скрипт ожидает чтобы ты ВПИСАЛ tokens сам перед запуском (placeholder ниже)
|
||||||
|
# - НЕ коммить скрипт с реальными токенами!
|
||||||
|
# - После запуска env vars в registry — можешь удалить локальную копию
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# REPLACE PLACEHOLDERS BELOW WITH ACTUAL TOKENS FROM VAULT
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$tokens = @{
|
||||||
|
"FORGEJO_TOKEN_ANALYST" = "REPLACE_ME_FROM_VAULT"
|
||||||
|
"FORGEJO_TOKEN_BACKEND" = "REPLACE_ME_FROM_VAULT"
|
||||||
|
"FORGEJO_TOKEN_FRONTEND" = "REPLACE_ME_FROM_VAULT"
|
||||||
|
"FORGEJO_TOKEN_REVIEWER" = "REPLACE_ME_FROM_VAULT"
|
||||||
|
"FORGEJO_TOKEN_QA" = "REPLACE_ME_FROM_VAULT"
|
||||||
|
"FORGEJO_URL_BOTS" = "https://git.gendsgn.ru"
|
||||||
|
"FORGEJO_REPO_BOTS" = "lekss361/gendesign"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Validation
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$placeholderCount = ($tokens.Values | Where-Object { $_ -eq "REPLACE_ME_FROM_VAULT" }).Count
|
||||||
|
if ($placeholderCount -gt 0) {
|
||||||
|
Write-Error "❌ $placeholderCount placeholders не заменены. Открой vault meta/00_credentials.md, секция 'Forgejo bot-users', и вставь реальные PAT'ы вместо REPLACE_ME_FROM_VAULT."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Apply
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Host "=== Setting 7 User-scope env vars (persistent) ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
foreach ($k in $tokens.Keys) {
|
||||||
|
[System.Environment]::SetEnvironmentVariable($k, $tokens[$k], "User")
|
||||||
|
Set-Item -Path "env:$k" -Value $tokens[$k]
|
||||||
|
|
||||||
|
if ($k -like "FORGEJO_TOKEN_*") {
|
||||||
|
$suffix = $tokens[$k].Substring($tokens[$k].Length - 4)
|
||||||
|
Write-Host " $k = ****$suffix"
|
||||||
|
} else {
|
||||||
|
Write-Host " $k = $($tokens[$k])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Verify
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Verify (read back from User scope) ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$allOK = $true
|
||||||
|
foreach ($k in $tokens.Keys) {
|
||||||
|
$stored = [System.Environment]::GetEnvironmentVariable($k, "User")
|
||||||
|
if ($stored -eq $tokens[$k]) {
|
||||||
|
Write-Host " $k`: OK (len=$($stored.Length))"
|
||||||
|
} else {
|
||||||
|
Write-Host " $k`: MISMATCH (stored len=$($stored.Length))" -ForegroundColor Red
|
||||||
|
$allOK = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
if ($allOK) {
|
||||||
|
Write-Host "✓ All 7 env vars set successfully." -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Запуск окна:"
|
||||||
|
Write-Host " claude"
|
||||||
|
Write-Host " # в чате:"
|
||||||
|
Write-Host " /work-as-backend # любая из 5: analyst/backend/frontend/reviewer/qa"
|
||||||
|
Write-Host " # pre-flight выполнится сам через env vars"
|
||||||
|
Write-Host " /loop dynamic # или другой cadence"
|
||||||
|
} else {
|
||||||
|
Write-Host "✗ Some env vars failed to set." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue