jira-agent/README.md
Andrey Epifancev 54f65d0333 docs(agent): README (доставка, запуск, launchd, prune), архитектура, проверка синтаксиса 3.9
README: доставка на виртуалку, check/запуск/launchd, журнал и prune, поведение с командами,
известные ограничения (prune при работе агента, Ctrl-C во время паузы, https без CA, спринты
закрытых спринтов, refresh, сравнение описания, 429/503 без потолка). CLAUDE.md и ARCHITECTURE.md
заполнены по src/agent.py. tests/test_py39.py: разбор с feature_version=(3, 9) и разрешённый
список импортов (только stdlib).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
2026-10-12 00:10:47 +04:00

165 lines
11 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.

# jira-agent
Клиент моста Jira для виртуалки с доступом к Jira банка. Агент опрашивает хаб (`jira-hub` на Mac mini),
выполняет команды «желаемый результат» в Jira по правилу «проверь, потом сделай», ведёт журнал на диске
и возвращает хабу состояние задач. Один файл `src/agent.py`, только стандартная библиотека, Python 3.9.
Спека: `../jira-hub/openspec/changes/bridge-core/specs/agent/spec.md` (вместе с `commands/` и `hub-api/`).
Хаб, очередь команд и зеркало задач живут в репозитории `jira-hub`; агент с ним общается только по HTTP.
## Доставка на виртуалку
1. Скопировать `src/agent.py` и `src/agent.example.json` через общую папку в каталог на виртуалке, например `~/jira-agent/`.
2. Переименовать `agent.example.json` в `agent.json` и заполнить:
- `hub_url` — `http://192.168.0.106:8790`;
- `hub_token` — агентский токен из `jira-hub tokens` на Mac mini;
- `jira.base_url` — адрес Jira банка;
- `jira.auth` — `{"type": "basic", "user": ..., "password": ...}` или `{"type": "bearer", "token": ...}`;
- `verify_ssl`, `ca_bundle` — при корпоративном сертификате (см. ограничение 3 ниже).
3. `agent.json` содержит учётные данные Jira. Никуда не копировать и никуда не отправлять, кроме этой виртуалки.
Хаб получает от агента только результаты команд и состояние задач, без паролей и токенов Jira.
Остальные настройки (id полей, типы задач, карта переходов, типы связей, доска, темп) агент получает от хаба при
старте и при каждом изменении `settings_version`; в `agent.json` их быть не должно.
## Проверка
```
python3 agent.py check
```
В норме печатает две строки: `хаб: доступен, настройки v<N>, проект <KEY>` и `Jira: доступен, пользователь <login>`.
Код выхода 0. Если хаб или Jira недоступны, строка содержит `НЕДОСТУПЕН` и текст ошибки, код выхода 1.
Команда ничего не пишет в Jira.
## Запуск
В терминале:
```
python3 agent.py
```
В фоне с логом:
```
nohup python3 -u agent.py > agent.log 2>&1 &
```
Остановка: Ctrl-C или `kill <pid>`. Первый сигнал — агент доделывает текущую команду, отправляет отчёт и выходит.
Повторный сигнал — немедленный выход (код 130). Команда, которая не успела записаться в журнал `started`, при этом
остаётся `claimed` в хабе (см. ограничение 2).
## Автозапуск через launchd (опционально)
Пример `~/Library/LaunchAgents/ru.aepif.jira-agent.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>ru.aepif.jira-agent</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>-u</string>
<string>/Users/USER/jira-agent/agent.py</string>
</array>
<key>WorkingDirectory</key><string>/Users/USER/jira-agent</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/USER/jira-agent/agent.log</string>
<key>StandardErrorPath</key><string>/Users/USER/jira-agent/agent.log</string>
</dict>
</plist>
```
Подставить свой путь вместо `USER`. Запуск и остановка:
```
launchctl load ~/Library/LaunchAgents/ru.aepif.jira-agent.plist
launchctl unload ~/Library/LaunchAgents/ru.aepif.jira-agent.plist
```
`WorkingDirectory` нужен, чтобы `agent.json` и `journal.jsonl` лежали рядом с `agent.py`.
## Журнал
`journal.jsonl` лежит рядом с `agent.py`. Это только дозапись: на каждую команду пишутся `started` (после паузы,
перед обращением к Jira), `finished` (результат и счётчики вызовов) и `acked` (хаб подтвердил приём отчёта).
По журналу агент после перезапуска досылает неподтверждённые отчёты и находит команды, начатые, но не завершённые.
Журнал содержит параметры команд, то есть тексты задач и комментариев. Не выносить его с виртуалки.
Очистка подтверждённых записей старше 30 дней:
```
python3 agent.py prune
```
Не запускать при работающем агенте (ограничение 1 ниже).
## Что делает агент с командами
Типы команд описаны в спеке `commands/spec.md`. Здесь — что агент делает с каждым:
| Тип | Что делает |
|---|---|
| `ensure_issue` | ищет задачу своей темы (`reporter = currentUser()`, за 7 дней), иначе создаёт; возвращает `key` |
| `ensure_fields` | читает задачу и пишет только отличающиеся поля |
| `ensure_comment` | комментарий с маркером в начале: обновляет, если текст изменился, иначе создаёт |
| `ensure_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных |
| `ensure_link` | создаёт связь; конфликт без `replace` — `failed`; с `replace` сначала удаляет лишнее |
| `ensure_sprint` | переносит в спринт доски (активные и будущие) или в бэклог |
| `refresh` | только читает Jira и отправляет состояние задач в хаб; ничего не пишет |
Правила работы:
- **Проверь, потом сделай.** Каждый тип сначала читает текущее состояние и записывает только разницу.
- **Паузы.** Хаб выдаёт команду с `pause_seconds` — агент досыпает до зазора от последнего вызова Jira. Внутри команды
с несколькими записями зазор берётся из `pacing` настроек хаба.
- **Одна команда в полёте.** Агент берёт следующую команду, только когда предыдущая выполнена и отчёт подтверждён.
- **Отчёт до подтверждения.** Отчёт повторяется каждые 10 с, пока хаб не подтвердит. Ответы 404/409/422 хаб
выдаёт, когда команда уже отменена или удалена: агент считает её закрытой и не зацикливается.
- **429 и 503.** Агент ждёт `Retry-After` из ответа Jira и повторяет запрос, до 5 попыток. Если заголовка нет,
ждёт 60 с, умноженные на номер попытки. Число ожиданий идёт в `rate_limited` отчёта.
- **Версия.** Константа `AGENT_VERSION` в `agent.py` (сейчас 1) отправляется в каждом `claim`.
Если хаб назначил `min_agent_version` выше, он пишет в `agent_status.warning`: «обновите агента»; команды
при этом выдаются по-прежнему.
## Тесты
```
/usr/bin/python3 -m unittest discover -s tests -t .
```
Тот же Python 3.9.6, что на виртуалке. Внешних сервисов нет: Jira и хаб заменяют фейки
(`tests/fake_jira.py`, `tests/fake_hub.py`). `tests/test_py39.py` проверяет, что `agent.py` разбирается как Python 3.9
и импортирует только стандартную библиотеку.
## Обновление агента
Скопировать новый `agent.py` поверх старого и перезапустить процесс (или `launchctl unload` + `load`).
`agent.json` и `journal.jsonl` не трогать: журнал переживает обновление, а неподтверждённые отчёты досылаются при старте.
## Известные ограничения / эксплуатация
В порядке важности:
1. **`prune` не запускать при работающем агенте.** Блокировки нет. Запись, добавленная агентом во время `prune`,
может пропасть при перезаписи файла. Сначала остановить агента.
2. **Ctrl-C во время паузы перед командой.** Команда остаётся `claimed` в хабе, в журнале о ней ничего нет.
Хаб сам её не переиздаёт. Лечение: в хабе отменить команду с force и повторить:
`POST /commands/{id}/cancel` с телом `{"force": true}`, затем `POST /commands/{id}/retry`.
3. **HTTPS хаба с корпоративным CA не поддержан.** `HubClient` работает без `ca_bundle` и `verify_ssl`: `hub_url`
должен быть `http://`. Для Jira `verify_ssl` и `ca_bundle` работают.
4. **Спринт в `refresh` — только активные и будущие спринты доски.** Поле `sprint` берётся из Agile API по
этим спринтам (рулинг R-6). У задач закрытых спринтов `sprint = null`.
5. **Особенности `refresh`.** В `result.issues` попадают только запрошенные задачи, соседи по связям — в
`result.neighbors`. Если `jql` вернул пустую выдачу, состояние в хаб не отправляется (`pages = 0`).
Отсутствующие ключи при `refresh` по `keys` попадают в `result.missing` (не больше 50).
6. **Сравнение описания в `ensure_fields` точное.** Если Jira нормализует переводы строк (CRLF), будут лишние
записи. Наблюдать на реальной Jira.
7. **429/503 без потолка.** Агент ждёт `Retry-After` сколько бы он ни был (без верхней границы), до 5 попыток.
Длинный `Retry-After` держит очередь.