diff --git a/.claude/agents/_autonomous_pickup.md b/.claude/agents/_autonomous_pickup.md index de1e680e..d36af51f 100644 --- a/.claude/agents/_autonomous_pickup.md +++ b/.claude/agents/_autonomous_pickup.md @@ -15,42 +15,53 @@ 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 +# 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 сначала" +} ``` -⚠️ Проверь что `~/.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..171c9ebe --- /dev/null +++ b/.forgejo/workflows/stale-claims.yml @@ -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 diff --git a/scripts/cleanup-stale-claims.sh b/scripts/cleanup-stale-claims.sh new file mode 100644 index 00000000..3eebf60c --- /dev/null +++ b/scripts/cleanup-stale-claims.sh @@ -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 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 +}