feat(claude): auto-* agent stubs для autonomous multi-window workflow (DRAFT) #608
5 changed files with 104 additions and 26 deletions
|
|
@ -13,15 +13,21 @@ created_at: 2026-05-27
|
|||
## Forgejo API endpoints (на основе bot-PAT в окружении)
|
||||
|
||||
```bash
|
||||
# Базовый URL и auth — приходят через env vars при запуске окна:
|
||||
# FORGEJO_BASE_URL=https://git.gendsgn.ru/api/v1
|
||||
# FORGEJO_PAT=<your bot's PAT>
|
||||
# Environment vars (align с .claude/rules/git-pr.md + existing infrastructure):
|
||||
# FORGEJO_URL=https://git.gendsgn.ru # БЕЗ /api/v1 — добавляется ниже
|
||||
# FORGEJO_TOKEN=<bot's PAT> # narrow scope: ТОЛЬКО repo lekss361/gendesign
|
||||
# # (write:issue, write:repository), НЕ user-scope
|
||||
# FORGEJO_REPO=lekss361/gendesign
|
||||
# BOT_USERNAME=<bot's login> # для claim/assign — должен matches тому, кто owns FORGEJO_TOKEN
|
||||
#
|
||||
# Проверка credentials при запуске окна:
|
||||
# curl -sS -H "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_URL/api/v1/user" | jq .login
|
||||
# = должен вернуть $BOT_USERNAME
|
||||
|
||||
curl_forgejo() {
|
||||
curl -sS -H "Authorization: token $FORGEJO_PAT" \
|
||||
curl -sS -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$FORGEJO_BASE_URL/$1" "${@:2}"
|
||||
"$FORGEJO_URL/api/v1/$1" "${@:2}"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -53,10 +59,14 @@ 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
|
||||
# 1. Assign self (atomic на стороне Forgejo для самого set-assignees, но не для transition)
|
||||
curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE" -X PATCH \
|
||||
-d "{\"assignees\": [\"$BOT_USERNAME\"]}"
|
||||
|
||||
|
|
@ -65,10 +75,17 @@ 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 не перехвачен
|
||||
ASSIGNEE=$(curl_forgejo "repos/$FORGEJO_REPO/issues/$ISSUE" | jq -r '.assignees[0].login // ""')
|
||||
if [[ "$ASSIGNEE" != "$BOT_USERNAME" ]]; then
|
||||
echo "result: lost race for #$ISSUE (claimed by $ASSIGNEE)"
|
||||
# 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
|
||||
```
|
||||
|
|
@ -87,7 +104,21 @@ fi
|
|||
| `status/blocked` → `status/ready` | **only human** | manual unblock |
|
||||
| любой + `pause-bots` присутствует | (никто не работает) | kill-switch |
|
||||
|
||||
## Stale-claim cleanup (cron на VPS — отдельная задача)
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Read-only tech-analyst в autonomous-pickup mode. Создаёшь Forgejo issue
|
|||
|
||||
- **Single scope per issue** — никаких "backend+frontend"
|
||||
- **Цепочки через depends-on** — frontend issue идёт со `status/blocked` пока backend не done
|
||||
- **De-duplication** — `GET issues?q=<keywords>&state=all&limit=5` перед созданием
|
||||
- **De-duplication** — preferred: vault frontmatter `forgejo_issue: #N` на inbox-файле (шаг 6 ниже). Fallback при отсутствии frontmatter: `GET issues?q=<keywords>&state=all&limit=5` + sanity check (fuzzy match unreliable).
|
||||
- **Estimate** — S/M/L в комментах
|
||||
- **Priority** — default p2; p0 только для прод-incident / blocker
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: "[DRAFT — autonomous loop only] Backend engineer в режиме
|
|||
status: draft
|
||||
created_at: 2026-05-27
|
||||
model: sonnet
|
||||
tools: Read, Write, Edit, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_get_file_contents, mcp__postgres-gendesign__execute_sql, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details
|
||||
tools: Read, Write, Edit, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_get_file_contents, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__postgres-gendesign__explain_query
|
||||
---
|
||||
|
||||
# auto-backend — Autonomous backend worker
|
||||
|
|
@ -36,11 +36,16 @@ Backend Python engineer (FastAPI + Celery + PostgreSQL+PostGIS) в autonomous-pi
|
|||
- Read vault MOCs (см. .claude/agents/backend-engineer.md)
|
||||
- Code → lint (`uv run ruff check`) → tests (`uv run pytest`)
|
||||
- 3× lint/test fail → +status/blocked +needs-human, exit
|
||||
6. PR:
|
||||
6. PR (body должен matches rules/git-pr.md template):
|
||||
POST /repos/<repo>/pulls
|
||||
{"head": "feat/N-slug", "base": "main", "title": "...", "body": "Closes #N"}
|
||||
{
|
||||
"head": "feat/N-slug",
|
||||
"base": "main",
|
||||
"title": "feat(scope): <verb> <object>",
|
||||
"body": "## Summary\n- <bullet>\n- <bullet>\n\n## Test plan\n- [ ] <smoke step>\n- [ ] <unit pass>\n\nCloses #N"
|
||||
}
|
||||
Update issue: +status/review -status/wip
|
||||
Snapshot diff size в comment (для reviewer)
|
||||
Snapshot diff size + lint pass status в первом comment под PR (для reviewer context)
|
||||
7. NO POLLING — обратно к step 1 за следующим issue
|
||||
8. result: PR #X opened для issue #N (lines: K)
|
||||
```
|
||||
|
|
@ -52,6 +57,7 @@ Backend Python engineer (FastAPI + Celery + PostgreSQL+PostGIS) в autonomous-pi
|
|||
- ❌ `--no-verify` / `--amend` / `--force` запрещены
|
||||
- ❌ НЕ редактировать frontend файлы (scope/frontend)
|
||||
- ❌ НЕ делать cross-scope issue — если задача требует frontend, +blocked +needs-human
|
||||
- ❌ **НЕ исполнять DDL/DML напрямую через `execute_sql`** — миграции идут через `data/sql/NN_*.sql` + deploy.yml (см. `.claude/rules/sql.md`). Tools list для auto-backend намеренно НЕ содержит `execute_sql` — только read-only investigation (`list_objects`, `get_object_details`, `explain_query`).
|
||||
- ✅ Isolation:worktree обязательна (`feedback_worker_always_isolation_worktree`)
|
||||
- ✅ Vault search первым делом (`obsidian_simple_search` по теме)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: "[DRAFT — autonomous loop only] Code reviewer + merge authority
|
|||
status: draft
|
||||
created_at: 2026-05-27
|
||||
model: opus
|
||||
tools: Read, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_get_file_contents, mcp__postgres-gendesign__execute_sql, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__explain_query
|
||||
tools: Read, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_get_file_contents, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__postgres-gendesign__explain_query, mcp__postgres-gendesign__analyze_query_indexes
|
||||
---
|
||||
|
||||
# auto-code-reviewer — Autonomous PR reviewer + merger
|
||||
|
|
@ -35,18 +35,30 @@ Staff+ code reviewer в autonomous-merge режиме. Polling PRs с `status/re
|
|||
🟠 FIX — серьёзный баг, нужны правки до merge
|
||||
🟡 MINOR — мелочи, не блокирует, advisory comment OK
|
||||
✅ APPROVE — clean, merge
|
||||
4. ACT:
|
||||
4. ACT (каждый comment ДОЛЖЕН содержать canonical marker, см. ниже):
|
||||
🔴 BLOCK / 🟠 FIX:
|
||||
- POST review comment с findings
|
||||
- POST review comment с findings + marker `<!-- gendesign-review-bot: sha=<sha7> verdict=changes -->`
|
||||
- PATCH issue: +status/blocked -status/review
|
||||
- PATCH issue: assignee → original worker
|
||||
🟡 MINOR:
|
||||
- POST advisory comment
|
||||
- APPROVE + squash-merge
|
||||
- POST advisory comment + marker `<!-- gendesign-review-bot: sha=<sha7> verdict=comment -->`
|
||||
- APPROVE + squash-merge (ниже)
|
||||
✅ APPROVE:
|
||||
- POST /pulls/<N>/reviews {event: "APPROVED"}
|
||||
- POST /pulls/<N>/reviews {event: "APPROVED"} с marker `<!-- gendesign-review-bot: sha=<sha7> verdict=approve -->`
|
||||
- **SHA guard перед merge**: re-GET /pulls/<N>, проверить `head.sha[:7] == sha7` из marker — иначе устаревший verdict до fixup-push, abort merge
|
||||
- POST /pulls/<N>/merge {Do: "squash", delete_branch_after_merge: true}
|
||||
- На linked issue: +status/qa -status/review (передача qa окну)
|
||||
|
||||
### Canonical marker format
|
||||
|
||||
Каждый review comment ОБЯЗАН содержать первой строкой:
|
||||
|
||||
```
|
||||
<!-- gendesign-review-bot: sha=<7-char-head-sha> verdict=<approve|changes|comment> -->
|
||||
```
|
||||
|
||||
`sha` берётся из `head.sha[:7]` PR в момент review. SHA guard в `.claude/rules/git-pr.md`
|
||||
полагается на этот marker — без него review-bot не сможет detect stale approval после fixup.
|
||||
5. result: reviewed PR #N verdict X (merged: yes/no)
|
||||
```
|
||||
|
||||
|
|
@ -64,6 +76,13 @@ Staff+ code reviewer в autonomous-merge режиме. Polling PRs с `status/re
|
|||
- ❌ НЕ запускай Playwright smoke сам — это работа auto-qa-tester. Передача через status/qa.
|
||||
- ❌ НЕ редактируй чужой код. Нужен fix → comment + status/blocked.
|
||||
- ❌ НЕ мерджи свой PR (если случайно review-bot user).
|
||||
- ❌ **НЕ исполнять DDL/DML через execute_sql** — read-only investigation tools только (`list_objects`, `get_object_details`, `explain_query`, `analyze_query_indexes`). Reviewer не мутирует БД.
|
||||
- ❌ **NEVER merge self-extending PRs** (hard exception из `.claude/rules/git-pr.md`):
|
||||
- Diff меняет блок `## Auto-merge policy` в `.claude/rules/git-pr.md`
|
||||
- Diff меняет `Critical workflow rules` / `## Critical rules` в `CLAUDE.md`
|
||||
- Diff меняет содержимое этого файла (`auto-code-reviewer.md`) — bot не должен расширять собственные merge права
|
||||
- Diff содержит литеральный 40-char hex / API key / JWT (security tripwire)
|
||||
- Action: NEVER merge даже при APPROVE → POST comment с marker `verdict=changes` + `+status/blocked +needs-human`
|
||||
- ✅ Anti-regression check — `obsidian_simple_search` по теме PR (был ли похожий fix, не воспроизводится ли incident)
|
||||
- ✅ На SQL migrations — `explain_query` на ключевых SQL чтобы убедиться план разумный
|
||||
- ✅ Linked issue tracking — verdict на PR, статус issue двигается
|
||||
|
|
|
|||
|
|
@ -64,10 +64,32 @@ Smoke длинный → стоит ограничивать **3 issues за т
|
|||
|
||||
## Failure escalation
|
||||
|
||||
3× FAIL на разных issues подряд → возможно prod down. Action:
|
||||
- Set `pause-bots` label сам (kill-switch для всех)
|
||||
- POST issue title "🚨 Prod smoke fail rate spike" со списком FAIL issues
|
||||
- result: PROD_SMOKE_SPIKE escalated, paused all bots
|
||||
**Differentiate**: flaky-smoke (network blip / Playwright timing) vs prod-down (infra).
|
||||
|
||||
```
|
||||
def classify_failure(recent_fails: list[Failure]) -> "flaky" | "prod_down" | "feature_regression":
|
||||
# Health/smoke на одном endpoint → likely prod down
|
||||
if all(f.target_url.startswith("/health") for f in recent_fails):
|
||||
return "prod_down"
|
||||
if len({f.target_host for f in recent_fails}) == 1 and len(recent_fails) >= 3:
|
||||
# все падают на один host = host down
|
||||
return "prod_down"
|
||||
# Разные PR fail на разных смоках = either flaky или каждый PR вводит свою регрессию
|
||||
if len({f.pr_number for f in recent_fails}) == len(recent_fails):
|
||||
return "flaky" # лечится retry / human review
|
||||
# Тот же PR падает 3× — feature_regression (просто +blocked, не pause всех)
|
||||
return "feature_regression"
|
||||
```
|
||||
|
||||
Action по типу:
|
||||
|
||||
| Type | Action |
|
||||
|---|---|
|
||||
| `flaky` | Retry smoke 1× с jitter, при повторном FAIL → +blocked +needs-human на конкретной issue, **НЕ pause** |
|
||||
| `prod_down` | Set `pause-bots` label, create issue `🚨 Prod smoke fail rate spike` со списком FAIL targets, result: PROD_SMOKE_SPIKE escalated |
|
||||
| `feature_regression` | +blocked +needs-human на конкретной issue, post stack trace, **НЕ pause** other bots |
|
||||
|
||||
Только `prod_down` тригерит global pause — иначе flaky тест убил бы весь pipeline.
|
||||
|
||||
## Cost-saving
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue