- CLAUDE.md: восстановлен раздел «OpenSpec и superpowers» из шаблона; убрана фраза про задачу 7; тесты описаны по областям; оговорка про проверку 3.9 (ast не ловит API, только прогон на 3.9.6). - README: пример вывода check с префиксом времени; timeout и poll_seconds в доставке; 429/503 — не более 4 ожиданий, Retry-After в HTTP-date не разбирается. - ARCHITECTURE.md: формулировка повторов 429/503. - tests/test_py39.py: комментарий с ограничением проверки. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
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.
Доставка на виртуалку
- Скопировать
src/agent.pyиsrc/agent.example.jsonчерез общую папку в каталог на виртуалке, например~/jira-agent/. - Переименовать
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).
agent.jsonсодержит учётные данные Jira. Никуда не копировать и никуда не отправлять, кроме этой виртуалки. Хаб получает от агента только результаты команд и состояние задач, без паролей и токенов Jira.
Остальные настройки (id полей, типы задач, карта переходов, типы связей, доска, темп) агент получает от хаба при
старте и при каждом изменении settings_version; в agent.json их быть не должно.
Проверка
python3 agent.py check
В норме печатает две строки с префиксом времени [ЧЧ:ММ:СС], например:
[10:42:17] хаб: доступен, настройки v3, проект AURORA
[10:42:18] Jira: доступен, пользователь ivanov
(хаб: доступен, настройки 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 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 запросов, то есть не более 4 ожиданий; на пятой неудаче — ошибка команды. Если заголовка нет или он в формате HTTP-date (не разбирается), ждёт 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 не трогать: журнал переживает обновление, а неподтверждённые отчёты досылаются при старте.
Известные ограничения / эксплуатация
В порядке важности:
pruneне запускать при работающем агенте. Блокировки нет. Запись, добавленная агентом во времяprune, может пропасть при перезаписи файла. Сначала остановить агента.- Ctrl-C во время паузы перед командой. Команда остаётся
claimedв хабе, в журнале о ней ничего нет. Хаб сам её не переиздаёт. Лечение: в хабе отменить команду с force и повторить:POST /commands/{id}/cancelс телом{"force": true}, затемPOST /commands/{id}/retry. - HTTPS хаба с корпоративным CA не поддержан.
HubClientработает безca_bundleиverify_ssl:hub_urlдолжен бытьhttp://. Для Jiraverify_sslиca_bundleработают. - Спринт в
refresh— только активные и будущие спринты доски. Полеsprintберётся из Agile API по этим спринтам (рулинг R-6). У задач закрытых спринтовsprint = null. - Особенности
refresh. Вresult.issuesпопадают только запрошенные задачи, соседи по связям — вresult.neighbors. Еслиjqlвернул пустую выдачу, состояние в хаб не отправляется (pages = 0). Отсутствующие ключи приrefreshпоkeysпопадают вresult.missing(не больше 50). - Сравнение описания в
ensure_fieldsточное. Если Jira нормализует переводы строк (CRLF), будут лишние записи. Наблюдать на реальной Jira. - 429/503 без потолка. Агент ждёт
Retry-Afterв секундах сколько бы он ни был (без верхней границы): не более 4 ожиданий, на пятой неудаче — ошибка. ДлинныйRetry-Afterдержит очередь. Формат HTTP-date вRetry-Afterне разбирается: тогда ждётся 60 с × номер попытки.