# 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 ниже); - `timeout` — таймаут запроса в секундах (по умолчанию 60); - `poll_seconds` — пауза между опросами хаба, когда команд нет (по умолчанию 5). 3. `agent.json` содержит учётные данные Jira. Никуда не копировать и никуда не отправлять, кроме этой виртуалки. Хаб получает от агента только результаты команд и состояние задач, без паролей и токенов Jira. Остальные настройки (id полей, типы задач, карта переходов, типы связей, доска, темп) агент получает от хаба при старте и при каждом изменении `settings_version`; в `agent.json` их быть не должно. ## Проверка ``` python3 agent.py check ``` В норме печатает две строки с префиксом времени `[ЧЧ:ММ:СС]`, например: ``` [10:42:17] хаб: доступен, настройки v3, проект AURORA [10:42:18] Jira: доступен, пользователь ivanov ``` (`хаб: доступен, настройки v, проект ` и `Jira: доступен, пользователь `). Код выхода 0. Если хаб или Jira недоступны, строка содержит `НЕДОСТУПЕН` и текст ошибки, код выхода 1. Команда ничего не пишет в Jira. ## Запуск В терминале: ``` python3 agent.py ``` В фоне с логом: ``` nohup python3 -u agent.py > agent.log 2>&1 & ``` Остановка: Ctrl-C или `kill `. Первый сигнал — агент доделывает текущую команду, отправляет отчёт и выходит. Повторный сигнал — немедленный выход (код 130). Остановка во время паузы перед командой безопасна: команда уже записана в журнал как `claimed`, при следующем запуске агент запрашивает её у хаба и выполняет через проверку (без паузы). ## Автозапуск через launchd (опционально) Пример `~/Library/LaunchAgents/ru.aepif.jira-agent.plist`: ```xml Labelru.aepif.jira-agent ProgramArguments /usr/bin/python3 -u /Users/USER/jira-agent/agent.py WorkingDirectory/Users/USER/jira-agent RunAtLoad KeepAlive StandardOutPath/Users/USER/jira-agent/agent.log StandardErrorPath/Users/USER/jira-agent/agent.log ``` Подставить свой путь вместо `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`. Это только дозапись: на каждую команду пишутся `claimed` (сразу после получения из хаба, до паузы), `started` (после паузы, перед обращением к Jira), `finished` (результат и счётчики вызовов) и `acked` (хаб подтвердил приём отчёта). По журналу агент после перезапуска досылает неподтверждённые отчёты и находит команды, полученные или начатые, но не завершённые. В журнале нет параметров команд (текстов задач и комментариев): только id команды, статус, `result`, `error` и счётчики вызовов. Но `result` и `error` могут содержать ключи задач и фрагменты ответов Jira, поэтому журнал лучше не выносить с виртуалки. Очистка подтверждённых записей старше 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 запросов, то есть не более 4 ожиданий; на пятой неудаче — ошибка команды. Если заголовка нет или он в формате HTTP-date (не разбирается), ждёт 60 с, умноженные на номер попытки. Число ответов 429/503 идёт в `rate_limited` отчёта. - **Обрыв связи с Jira (R-10).** Если Jira оборвала соединение или ответила 5xx, исход записи неизвестен: агент не пишет `finished`, ждёт не меньше 30 с и повторяет команду через проверку («проверь, потом сделай») до 3 раз. Счётчики суммируются. После исчерпания — `failed` «результат неизвестен (обрыв связи с Jira), повтор безопасен». При остановке по сигналу команду подхватит восстановление. - **Версия.** Константа `AGENT_VERSION` в `agent.py` (сейчас 1) отправляется в каждом `claim`, в каждом отчёте (`agent_version`) и в `/agent/state`. Если хаб назначил `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. **Остановка во время паузы или между claim и выполнением (R-9).** Команда остаётся `claimed` в хабе, но в журнале есть запись `claimed`: при перезапуске агент запрашивает её у хаба и выполняет без паузы. Отмена с force и повтор больше не нужны. Если журнал потерян, команду придётся отменить с 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` в секундах сколько бы он ни был (без верхней границы): не более 4 ожиданий, на пятой неудаче — ошибка. Длинный `Retry-After` держит очередь. Формат HTTP-date в `Retry-After` не разбирается: тогда ждётся 60 с × номер попытки. Нецелый `Retry-After` (например, `1.5`) тоже не разбирается: те же 60 с × попытка.