diff --git a/.claude/agents/_autonomous_pickup.md b/.claude/agents/_autonomous_pickup.md index de1e680e..554d9edc 100644 --- a/.claude/agents/_autonomous_pickup.md +++ b/.claude/agents/_autonomous_pickup.md @@ -15,42 +15,50 @@ created_at: 2026-05-27 > **Это критично.** Без правильной настройки git identity → commits будут писаться > под user'ом (lekss361), не под ботом. Audit trail сломается. -### Шаг 1 — Получить PAT из vault +### Шаг 1 — Где живут PAT'ы -PAT'ы для всех 5 ботов хранятся в **`meta/00_credentials.md`** (vault, sensitive, -не в git). Один раз выгрузи их в локальные secrets: +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 -# Локация — никогда не коммитится (gitignored через ~/.gitignore_global) -New-Item -ItemType Directory -Force ~/.claude/secrets | Out-Null - -# Для каждого бота — скопируй PAT из vault в файл (открой meta/00_credentials.md, -# секция "Forgejo bot-users", найди PAT для bot-, вставь сюда): -# ~/.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): -# "" | Set-Content ~/.claude/secrets/forgejo-bot-.token -NoNewline +[System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User") | Out-Null +if (-not $?) { Write-Error "❌ Запусти setup-bot-env.ps1 сначала" } ``` -⚠️ Проверь что `~/.claude/secrets/` есть в `~/.gitignore_global` (или эквивалентном). - ### Шаг 2 — Env vars + git identity (per окно) -Замени `` на свою роль (`analyst`/`backend`/`frontend`/`reviewer`/`qa`): +Замени `` на свою роль (`ANALYST`/`BACKEND`/`FRONTEND`/`REVIEWER`/`QA` — UPPER-case): ```powershell -$ROLE = "backend" # ← ИЗМЕНИ ПЕРЕД ЗАПУСКОМ -$BOT = "bot-$ROLE" +$ROLE = "BACKEND" # ← ИЗМЕНИ ПЕРЕД ЗАПУСКОМ (UPPER-case) +$BOT = "bot-$($ROLE.ToLower())" -# Forgejo API credentials -$env:FORGEJO_TOKEN = (Get-Content ~/.claude/secrets/forgejo-$BOT.token -Raw).Trim() +# Resolve token из persistent User env +$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_$ROLE", "User") $env:BOT_USERNAME = $BOT -$env:FORGEJO_URL = "https://git.gendsgn.ru" -$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") + +# 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 diff --git a/.claude/commands/work-as-analyst.md b/.claude/commands/work-as-analyst.md index fdefde01..d2a56921 100644 --- a/.claude/commands/work-as-analyst.md +++ b/.claude/commands/work-as-analyst.md @@ -9,19 +9,28 @@ description: Запустить окно как auto-analyst (декомпози ## Pre-flight checks (выполни СЕЙЧАС, до /loop) -1. **Verify env vars set:** - - `$env:FORGEJO_TOKEN` (для bot-analyst — см. `meta/00_credentials.md`) - - `$env:BOT_USERNAME = "bot-analyst"` - - `$env:FORGEJO_URL = "https://git.gendsgn.ru"` - - `$env:FORGEJO_REPO = "lekss361/gendesign"` +```powershell +# 1. Resolve credentials из persistent User env +$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_ANALYST", "User") +$env:BOT_USERNAME = "bot-analyst" +$env:FORGEJO_URL = [System.Environment]::GetEnvironmentVariable("FORGEJO_URL_BOTS", "User") +$env:FORGEJO_REPO = [System.Environment]::GetEnvironmentVariable("FORGEJO_REPO_BOTS", "User") -2. **Verify identity:** - ```bash - curl -sH "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_URL/api/v1/user" | jq .login - # Должен вернуть "bot-analyst" - ``` +if (-not $env:FORGEJO_TOKEN) { + Write-Error "❌ FORGEJO_TOKEN_ANALYST не выставлен. Запусти scripts/setup-bot-env.ps1" + return +} -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 diff --git a/.claude/commands/work-as-backend.md b/.claude/commands/work-as-backend.md index ac06f402..383809af 100644 --- a/.claude/commands/work-as-backend.md +++ b/.claude/commands/work-as-backend.md @@ -9,35 +9,40 @@ description: Запустить окно как auto-backend (pickup scope/backe ## Pre-flight checks (выполни СЕЙЧАС, до /loop) -1. **Env vars (см. `.claude/agents/_autonomous_pickup.md` Шаги 1-2):** - ```powershell - $env:FORGEJO_TOKEN = Get-Content ~/.claude/secrets/forgejo-bot-backend.token -Raw - $env:BOT_USERNAME = "bot-backend" - $env:FORGEJO_URL = "https://git.gendsgn.ru" - $env:FORGEJO_REPO = "lekss361/gendesign" - ``` +```powershell +# 1. Resolve credentials из persistent User env (выставлены setup-bot-env.ps1) +$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_BACKEND", "User") +$env:BOT_USERNAME = "bot-backend" +$env:FORGEJO_URL = [System.Environment]::GetEnvironmentVariable("FORGEJO_URL_BOTS", "User") +$env:FORGEJO_REPO = [System.Environment]::GetEnvironmentVariable("FORGEJO_REPO_BOTS", "User") -2. **Git identity (КРИТИЧНО — иначе commits под lekss361):** - ```powershell - $env:GIT_AUTHOR_NAME = $env:BOT_USERNAME - $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 - ``` +if (-not $env:FORGEJO_TOKEN) { + Write-Error "❌ FORGEJO_TOKEN_BACKEND не выставлен. Запусти scripts/setup-bot-env.ps1" + return +} -3. **Bot-remote для push (audit-log под bot):** - ```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" - ``` +# 2. Git identity — КРИТИЧНО, иначе commits под lekss361 +$env:GIT_AUTHOR_NAME = $env:BOT_USERNAME +$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 -4. **Verify identity:** - ```bash - curl -sH "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_URL/api/v1/user" | jq .login - # Должен вернуть "bot-backend" - ``` +# 3. Bot-remote для push (audit log под bot, не lekss361) +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" -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 diff --git a/.claude/commands/work-as-frontend.md b/.claude/commands/work-as-frontend.md index 89793d62..baf961fe 100644 --- a/.claude/commands/work-as-frontend.md +++ b/.claude/commands/work-as-frontend.md @@ -9,9 +9,15 @@ description: Запустить окно как auto-frontend (pickup scope/fron ## Pre-flight checks -См. `.claude/commands/work-as-backend.md` для полного pre-flight — идентично, только: -- `BOT_USERNAME = "bot-frontend"` -- PAT из `~/.claude/secrets/forgejo-bot-frontend.token` +Идентично `work-as-backend.md`, только изменить две строки: + +```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 diff --git a/.claude/commands/work-as-qa.md b/.claude/commands/work-as-qa.md index 452dc5f4..b819be9c 100644 --- a/.claude/commands/work-as-qa.md +++ b/.claude/commands/work-as-qa.md @@ -9,9 +9,13 @@ description: Запустить окно как auto-qa-tester (Playwright smoke ## Pre-flight checks -Идентично `work-as-backend.md`, только: -- `BOT_USERNAME = "bot-qa"` -- PAT из `~/.claude/secrets/forgejo-bot-qa.token` +Идентично `work-as-backend.md`, только изменить две строки: + +```powershell +$env:FORGEJO_TOKEN = [System.Environment]::GetEnvironmentVariable("FORGEJO_TOKEN_QA", "User") +$env:BOT_USERNAME = "bot-qa" +# остальное — см. work-as-backend.md +``` **Дополнительно** — этот bot использует Playwright MCP, проверь что доступен: diff --git a/.claude/commands/work-as-reviewer.md b/.claude/commands/work-as-reviewer.md index 4afc5d84..982cfb80 100644 --- a/.claude/commands/work-as-reviewer.md +++ b/.claude/commands/work-as-reviewer.md @@ -9,9 +9,13 @@ description: Запустить окно как auto-code-reviewer (review + mer ## Pre-flight checks -Идентично `work-as-backend.md`, только: -- `BOT_USERNAME = "bot-reviewer"` -- PAT из `~/.claude/secrets/forgejo-bot-reviewer.token` +Идентично `work-as-backend.md`, только изменить две строки: + +```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 более строго: diff --git a/.forgejo/workflows/stale-claims.yml b/.forgejo/workflows/stale-claims.yml new file mode 100644 index 00000000..c760c751 --- /dev/null +++ b/.forgejo/workflows/stale-claims.yml @@ -0,0 +1,35 @@ +# 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 + +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 diff --git a/scripts/cleanup-stale-claims.sh b/scripts/cleanup-stale-claims.sh new file mode 100644 index 00000000..23a4ccee --- /dev/null +++ b/scripts/cleanup-stale-claims.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Cleanup stale claims for autonomous multi-agent workflow. +# +# Освобождает issues застрявшие в `status/wip` дольше 4 часов: +# - убирает assignee +# - меняет label status/wip → status/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) +# +# 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}" + +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.UTC).timestamp()))' +} + +iso_to_epoch() { + python3 -c "import datetime; print(int(datetime.datetime.fromisoformat('$1'.replace('Z','+00:00')).timestamp()))" +} + +echo "[$(date -Is)] cleanup-stale-claims: scanning status/wip issues (stale threshold: ${STALE_HOURS}h)" + +NOW=$(now_utc_epoch) +CUTOFF=$((NOW - STALE_SECS)) + +# GET all open issues с label status/wip +RESPONSE=$(curl_forgejo "repos/$FORGEJO_REPO/issues?labels=status/wip&state=open&limit=50&page=1") + +# Парсим JSON +echo "$RESPONSE" | python3 <= 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") + + # Build commands + 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. 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, + ) + + # 3. Remove status/wip label + subprocess.run( + ["curl", "-sS", "-X", "DELETE", *auth, + f"{base}/repos/{repo}/issues/{issue_num}/labels/status%2Fwip"], + 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(f"") +print(f"[OK] Released {released} stale claim(s), {errors} errors") +sys.exit(1 if errors else 0) +PYEOF diff --git a/scripts/setup-bot-env.ps1 b/scripts/setup-bot-env.ps1 new file mode 100644 index 00000000..b3bb5baa --- /dev/null +++ b/scripts/setup-bot-env.ps1 @@ -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 +}