feat: ядро моста Jira — агент (bridge-core, #1) #2

Merged
aep merged 30 commits from feat/1-bridge-core into main 2026-10-12 20:47:37 +03:00
Owner

Closes #1

Что сделано

Агент моста Jira на виртуалке банка (Python 3.9, только stdlib) — исполнитель команд хаба (aep/jira-hub PR #2, спека OpenSpec bridge-core живёт в хабе; здесь в openspec/changes/bridge-core/design.md — рулинги агента).

  • Клиенты Jira Server REST v2 / Agile и хаба (токен, Retry-After на 429, обработка обрывов и не-JSON ответов).
  • Журнал на диске (append-only, fsync, права 0600, prune): claimed → started → finished, восстановление после перезапуска.
  • Команды ensure_issue (поиск дубля с пагинацией по маркеру), ensure_fields, ensure_comment, ensure_status, ensure_link, ensure_sprint — «проверь, потом сделай», идемпотентны.
  • refresh: выгрузка задач по JQL в хаб, спринты из Agile, бисекция ключей при 400.
  • Runner: claim → пауза из хаба → выполнение → отчёт; повтор до 3 раз при обрыве связи с Jira с ожиданием 30 с от момента отказа; корректная остановка по сигналу; agent_version в отчётах.
  • Фейковые Jira и хаб для тестов; README, CLAUDE.md, ARCHITECTURE.md; проверка совместимости с 3.9.

Проверка

  • /usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t . (Python 3.9.6) → Ran 119 tests, OK.
  • Прогон на реальной Jira банка и живом хабе не выполнялся — пункт чеклиста закрытия (CRLF в описании, очистка Epic Link через PUT null, постраничность GET /comment, 400 на key in (...), поле спринта).

Rulings I made

  • R-1 (design #1): tests/__init__.py добавляет tests/ в sys.path (иначе from _common import из плана не работает при discover -t .). Цена ошибки: мелкая, переписать импорты на относительные.
  • R-2 (#2): потолок на Retry-After не вводим — спека требует ждать. Цена: агент может уснуть надолго при аномальном заголовке.
  • R-3 (#3): не лечим права существующего журнала, prune/append не блокируем flock'ом — запрет prune при работающем агенте в README. Цена: редкая потеря записи журнала при гонке.
  • R-4 (#4): minor Task 3 чиним одной финальной волной. Цена: Task 4–5 унаследовали общий run (исправлено в волне).
  • R-5 (#5): неизвестный link_type — сырое имя типа Jira (как в хабе). Цена: опечатка даст HTTP-ошибку Jira, а не понятную ошибку агента.
  • R-6 (#6): sprint в refresh из Agile (активные/будущие спринты доски), закрытые → null. Цена: исторические имена спринтов затираются при полном refresh; лечится полем спринта в настройках хаба.
  • R-7 (#7): стоимость бисекции 400 принята без лимита; если не найден ни один ключ — failed. Цена: всплеск запросов при массовом удалении задач.
  • R-8 (#8): общий except Exception в Runner.execute; minor-находки — одной волной. Цена: ложный failed ensure_issue (закрыт R-10).
  • R-9 (#9): запись claimed в журнал до паузы; расширение формата журнала сверх спеки. Цена: лишний kind в журнале.
  • R-10 (#10): до 3 повторов команды через проверку при ConnectionError/HTTP 5xx без finished, затем failed «результат неизвестен, повтор безопасен». Цена: задержки при недоступной Jira.
  • R-11 (#11): запрошенное, но не настроенное поле (epic_key, planned_*) → failed. Цена: у свежего хаба с пустыми настройками такие команды падают до настройки.
  • R-12 (#12): agent_version в result и /agent/state. Цена: нет (хаб игнорирует лишние поля).
  • R-13 (#13): F-1 сначала отложен контроллером, затем исправлен по команде владельца 12.10.2026 (527fd89): ожидание перед повтором — max(pause, 30) от момента отказа. Цена: лишнее ожидание не дольше 30 с на попытку.

Находки финального ревью (opus)

Маркер Суть Статус
I-1 claimed до паузы fixed 6bcc6f6
I-2 ConnectionError → повтор fixed 6bcc6f6
I-3 http.client.HTTPException как обрыв fixed 2e6f47a
I-4 поиск дубля с пагинацией fixed 2e6f47a
I-5 ConfigGap (ненастроенное поле) fixed 2e6f47a
I-6 agent_version fixed 6bcc6f6
M-1…M-4, M-6 fixed 6bcc6f6
M-5, M-7 fixed 2e6f47a
M-8 документация fixed 2201831
M-12 неточность ledger про паузу в refresh fixed (оговорка в ledger)
F-1 повтор R-10 не ждал 30 с при старом last_call_at fixed 527fd89
M-9, M-10, M-11, FM-1…FM-4 см. Deferred deferred

Deferred (уйдут в follow-up issue при закрытии)

  • FM-1 — сон 60 с на 401 хаба не прерывается stop.
  • FM-2 — 503 после 5 внутренних попыток × 4 внешних.
  • FM-3 — формулировка «обрыв связи с Jira» для ошибок хаба.
  • FM-4 — повторная запись claimed в recover.
  • M-9 — темп после перезапуска не восстанавливается из журнала.
  • M-10 — точность фейков (окно created, project, epic=null, долгий опрос).
  • M-11 — наблюдения для хаба: report_result принимает отчёт по queued-команде; две ensure_issue с одной темой в пакете сопоставятся с одной задачей.
  • T1.9 — HubClient без ca_bundle/verify_ssl.
  • N-3 — тест не покрывает тексты HttpError в логах.
  • T2.3 — prune vs run (запрет в README, R-3); T2.7 — порядок unacked_finished; мелочи Task 2 (слабый тест UTC, fd leak при fdopen, E303, fsync каталога).
  • T3.2 — индекс Jira отстаёт при поиске дубля; T3.3 — флаги existed/created; T3.7 — базовые метки (передаёт хаб); T3.9 — CRLF в описании.
  • T4.7 — спринты по id; T4.10 — переход без to; T4.11 — target = имя перехода не идемпотентен.
  • T5.2 — нет лимита keys в refresh.
  • T7.6 — явный список stdlib.

Наблюдения

Маркер Суть Статус
O-1 Ctrl-C в паузе оставлял команду claimed без записи в журнале fixed 6bcc6f6 (R-9)
O-2 log() печатает в stdout, тесты шумят deferred
O-3 хаб check_confirmed сч��тает входящую симметричную связь конфликтом, агент — existed deferred (follow-up в jira-hub)
O-4 исполнители подписывали коммиты чужой моделью; переписаны amend до push fixed
O-5 исполнители пытались добавить gitignored-отчёты (git add -f) fixed (убрано до push)

🤖 Generated with Claude Code

Closes #1 ## Что сделано Агент моста Jira на виртуалке банка (Python 3.9, только stdlib) — исполнитель команд хаба (aep/jira-hub PR #2, спека OpenSpec `bridge-core` живёт в хабе; здесь в `openspec/changes/bridge-core/design.md` — рулинги агента). - Клиенты Jira Server REST v2 / Agile и хаба (токен, Retry-After на 429, обработка обрывов и не-JSON ответов). - Журнал на диске (append-only, fsync, права 0600, `prune`): `claimed` → `started` → `finished`, восстановление после перезапуска. - Команды `ensure_issue` (поиск дубля с пагинацией по маркеру), `ensure_fields`, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint` — «проверь, потом сделай», идемпотентны. - `refresh`: выгрузка задач по JQL в хаб, спринты из Agile, бисекция ключей при 400. - Runner: claim → пауза из хаба → выполнение → отчёт; повтор до 3 раз при обрыве связи с Jira с ожиданием 30 с от момента отказа; корректная остановка по сигналу; `agent_version` в отчётах. - Фейковые Jira и хаб для тестов; README, CLAUDE.md, ARCHITECTURE.md; проверка совместимости с 3.9. ## Проверка - `/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .` (Python 3.9.6) → Ran 119 tests, OK. - Прогон на реальной Jira банка и живом хабе **не выполнялся** — пункт чеклиста закрытия (CRLF в описании, очистка Epic Link через PUT null, постраничность GET /comment, 400 на `key in (...)`, поле спринта). ## Rulings I made - R-1 (design #1): `tests/__init__.py` добавляет `tests/` в `sys.path` (иначе `from _common import` из плана не работает при `discover -t .`). Цена ошибки: мелкая, переписать импорты на относительные. - R-2 (#2): потолок на Retry-After не вводим — спека требует ждать. Цена: агент может уснуть надолго при аномальном заголовке. - R-3 (#3): не лечим права существующего журнала, prune/append не блокируем flock'ом — запрет prune при работающем агенте в README. Цена: редкая потеря записи журнала при гонке. - R-4 (#4): minor Task 3 чиним одной финальной волной. Цена: Task 4–5 унаследовали общий run (исправлено в волне). - R-5 (#5): неизвестный link_type — сырое имя типа Jira (как в хабе). Цена: опечатка даст HTTP-ошибку Jira, а не понятную ошибку агента. - R-6 (#6): sprint в refresh из Agile (активные/будущие спринты доски), закрытые → null. Цена: исторические имена спринтов затираются при полном refresh; лечится полем спринта в настройках хаба. - R-7 (#7): стоимость бисекции 400 принята без лимита; если не найден ни один ключ — failed. Цена: всплеск запросов при массовом удалении задач. - R-8 (#8): общий `except Exception` в `Runner.execute`; minor-находки — одной волной. Цена: ложный failed `ensure_issue` (закрыт R-10). - R-9 (#9): запись `claimed` в журнал до паузы; расширение формата журнала сверх спеки. Цена: лишний kind в журнале. - R-10 (#10): до 3 повторов команды через проверку при ConnectionError/HTTP 5xx без `finished`, затем `failed` «результат неизвестен, повтор безопасен». Цена: задержки при недоступной Jira. - R-11 (#11): запрошенное, но не настроенное поле (epic_key, planned_*) → failed. Цена: у свежего хаба с пустыми настройками такие команды падают до настройки. - R-12 (#12): `agent_version` в result и `/agent/state`. Цена: нет (хаб игнорирует лишние поля). - R-13 (#13): F-1 сначала отложен контроллером, затем исправлен по команде владельца 12.10.2026 (527fd89): ожидание перед повтором — `max(pause, 30)` от момента отказа. Цена: лишнее ожидание не дольше 30 с на попытку. ## Находки финального ревью (opus) | Маркер | Суть | Статус | |---|---|---| | I-1 | `claimed` до паузы | fixed 6bcc6f6 | | I-2 | ConnectionError → повтор | fixed 6bcc6f6 | | I-3 | `http.client.HTTPException` как обрыв | fixed 2e6f47a | | I-4 | поиск дубля с пагинацией | fixed 2e6f47a | | I-5 | ConfigGap (ненастроенное поле) | fixed 2e6f47a | | I-6 | `agent_version` | fixed 6bcc6f6 | | M-1…M-4, M-6 | | fixed 6bcc6f6 | | M-5, M-7 | | fixed 2e6f47a | | M-8 | документация | fixed 2201831 | | M-12 | неточность ledger про паузу в refresh | fixed (оговорка в ledger) | | F-1 | повтор R-10 не ждал 30 с при старом `last_call_at` | fixed 527fd89 | | M-9, M-10, M-11, FM-1…FM-4 | см. Deferred | deferred | ## Deferred (уйдут в follow-up issue при закрытии) - FM-1 — сон 60 с на 401 хаба не прерывается stop. - FM-2 — 503 после 5 внутренних попыток × 4 внешних. - FM-3 — формулировка «обрыв связи с Jira» для ошибок хаба. - FM-4 — повторная запись `claimed` в recover. - M-9 — темп после перезапуска не восстанавливается из журнала. - M-10 — точность фейков (окно created, project, epic=null, долгий опрос). - M-11 — наблюдения для хаба: `report_result` принимает отчёт по queued-команде; две `ensure_issue` с одной темой в пакете сопоставятся с одной задачей. - T1.9 — HubClient без ca_bundle/verify_ssl. - N-3 — тест не покрывает тексты HttpError в логах. - T2.3 — prune vs run (запрет в README, R-3); T2.7 — порядок `unacked_finished`; мелочи Task 2 (слабый тест UTC, fd leak при fdopen, E303, fsync каталога). - T3.2 — индекс Jira отстаёт при поиске дубля; T3.3 — флаги existed/created; T3.7 — базовые метки (передаёт хаб); T3.9 — CRLF в описании. - T4.7 — спринты по id; T4.10 — переход без `to`; T4.11 — target = имя перехода не идемпотентен. - T5.2 — нет лимита keys в refresh. - T7.6 — явный список stdlib. ## Наблюдения | Маркер | Суть | Статус | |---|---|---| | O-1 | Ctrl-C в паузе оставлял команду `claimed` без записи в журнале | fixed 6bcc6f6 (R-9) | | O-2 | `log()` печатает в stdout, тесты шумят | deferred | | O-3 | хаб `check_confirmed` сч��тает входящую симметричную связь конфликтом, агент — existed | deferred (follow-up в jira-hub) | | O-4 | исполнители подписывали коммиты чужой моделью; переписаны amend до push | fixed | | O-5 | исполнители пытались добавить gitignored-отчёты (`git add -f`) | fixed (убрано до push) | 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 30 commits 2026-10-12 20:45:38 +03:00
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Поиск дубля в ensure_issue до создания (автор = currentUser(), точная тема).
ensure_fields пишет только отличающиеся поля, описание сравнивается точно,
даты — по первым 10 символам.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
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
- 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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
Обрыв соединения не обновляет last_call_at, поэтому пауза перед повтором,
считавшаяся от последнего успешного вызова, при старом значении была нулевой
и три повтора шли подряд. Ожидание вынесено в Runner.wait_seconds и для
повтора считается от момента отказа; wait_pause использует его же и больше
не принимает force. Запись R-13 в design.md обновлена.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
aep merged commit 3b9b3b35c2 into main 2026-10-12 20:47:37 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: aep/jira-agent#2
No description provided.