16 KiB
Context
Две машины в одной локальной сети: Mac mini (192.168.0.106, Python 3.13, uv, Docker не всегда запущен) и виртуалка
с macOS и системным Python 3.9.6 (192.168.0.112, входящих портов нет, pip3 есть, доступ к Jira банка
https://sdlc.go.rshbank.ru/jira по логину/паролю из config.json). Общая папка гипервизора — запасной канал.
Исходный код для переноса: admin/jira-sync/jira_sync.py (клиент REST v2 + Agile, темп Pacer, карта переходов,
исправленная семантика связей: в POST /issueLink inwardIssue — источник, в GET outwardIssue у X — цель X),
config.example.json (id полей, типы, карта переходов, типы связей, доска 4216).
Goals / Non-Goals
Goals: убрать ручной перенос файлов; один источник ключей; журнал и повторяемость изменений; безопасный повтор любой команды; темп, при котором Jira не отвечает 429; MCP, через который сессия ставит пакеты и читает состояние. Non-goals — см. proposal.md.
Decisions
- Два репозитория, один change. Спека и план лежат в
jira-hub;jira-agentссылается на них из CLAUDE.md. Агент меняется редко и живёт на другой машине, поэтому отдельный репозиторий без зависимостей от хаба. - Хаб — один процесс: SQLite + HTTP + MCP. Один писатель в базу, нет зависимости от Docker, база — один файл. Агент и интерфейсы ходят только через HTTP; виртуалке ничего не ставить, у неё нет входящих портов.
- Команды — желаемый результат (
ensure_*), каждая на одной задаче, выполняется как «прочитать из Jira → если результат уже есть,doneбез записи → иначе записать». Повтор безопасен по построению. - Хаб не повторяет команды. Взятая команда без ответа видна как «нет ответа от агента с …»; повторяет её только сам агент после перезапуска по своему журналу, и только через проверку. Отменить взятую команду может только человек.
- Истина в Jira. После каждого пакета хаб добавляет
refreshпо затронутым ключам; команда без ответа, результат которой виден в зеркале, помечаетсяconfirmed_by_state. - Политика темпа в хабе, пауза на команде. При выдаче команды хаб ставит «не раньше, чем через N с после предыдущей операции в Jira» по типу команды и типу предыдущей. Агент помнит только время последнего вызова Jira. Нет следующей команды — нет паузы. 429/503 — Retry-After в агенте (инструкция сервера, не политика).
- Пакет = одно действие пользователя. Команды пакета — по порядку, пакеты — по порядку постановки, одна команда в полёте.
Упавшая команда не останавливает пакет; команды, ссылающиеся на её результат, —
skipped. - Временные ключи
new:<n>живут в хабе:ensure_issueс временным ключом, послеdoneхаб подменяетnew:<n>в параметрах следующих команд пакета и вissue_note. У агента соответствия нет. - Зеркало — JQL из настроек (
project = AURORA AND labels = AWPCA); задачи, на которые ведут связи, но вне JQL, дочитываются по ключу и помечаютсяneighbor. - Служебные заметки отдельно от зеркала (
issue_note), чтобы обновление из Jira их не затирало. - Настройки агента приходят от хаба (id полей, карта переходов, типы связей, политика темпа, версия настроек); локально у агента — адрес хаба, токен, Jira и учётные данные.
Risks / Trade-offs
- Чтение перед каждой записью — лишний вызов на команду. Цена мала (пауза перед чтением 2 с), а гарантия идемпотентности стоит дороже.
- Поиск дубля при
ensure_issueпо точной теме и автору за 7 дней может не найти задачу, если тему успели поправить в Jira руками. Остаточный риск принят: окно между ответом Jira и записью журнала — миллисекунды. - Один агент и одна команда в полёте — медленно для сотни операций (как и сейчас: ~4,5 ч на полную актуализацию). Это цена темпа, не архитектуры.
- Хаб должен быть запущен для MCP; если нет — инструменты возвращают понятную ошибку, не падают.
- SQLite по сети не используется; при переезде хаба на другую машину — переносится файл.
Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 2026-10-11 | Хаб: SQLite в одном процессе с HTTP, без PostgreSQL | один писатель, Docker здесь не всегда поднят, виртуалке ничего не ставить; владелец отдал плумбинг исполнителю | смена хранилища — замена одного модуля хаба, API не меняется |
| 2026-10-11 | HTTP-библиотека хаба — stdlib http.server + ThreadingHTTPServer, без FastAPI |
API маленький, зависимостей ноль, тесты через urllib; MCP — пакет mcp (FastMCP) через uv |
при росте API — переезд на FastAPI внутри того же репозитория |
| 2026-10-11 | Порт хаба 8790, привязка к 0.0.0.0 |
свободный порт; виртуалка в LAN с другим адресом, 127.0.0.1 ей недоступен |
пересечение портов — поправить настройку |
| 2026-10-11 | claim — долгий запрос до 30 с, затем сразу следующий |
минимальная задержка выдачи без постоянного трафика | — |
| 2026-10-11 | Поиск дубля в ensure_issue: project = AURORA AND reporter = currentUser() AND created >= -7d + точное сравнение темы на клиенте |
JQL summary ~ нечёткий; точное сравнение в агенте надёжнее |
дубль при смене темы руками в первые 7 дней; хаб показывает такие команды отдельно |
| 2026-10-11 | Исполнитель входит в ensure_fields |
11.10 исполнители ставились руками, в просмотрщике это нужная операция; владелец согласился | — |
| 2026-10-11 | Паузы по умолчанию: изменение задачи 120 с, комментарий и переход 30 с, чтение 2 с, создание связи 30 с | значения pacing из config.example.json, проверенные на Jira банка 10–11.10 |
настройки, правятся без кода |
| 2026-10-11 | Резервная копия файла базы перед каждым пакетом в backups/, хранить последние 30 |
заменяет историю версий зеркала | место на диске; число в настройках |
| 2026-10-11 | Хранение базы и настроек хаба — ~/.local/share/jira-hub/ |
вне git, переживает пересоздание worktree | — |
| 2026-10-11 | Регистрация MCP — в ~/.claude.json для проекта awp-ca (stdio, uv run jira-hub mcp) |
как остальные MCP этого проекта | — |
| 2026-10-11 | Полная синхронизация по расписанию: хаб ставит refresh по базовой JQL раз в 60 мин, если агент на связи и очередь пуста |
свежее зеркало без участия человека | лишние чтения; интервал в настройках, 0 отключает |
| 2026-10-11 | R-1: Review Focus #5 плана ссылается на Task 7, тест test_settings_put_rejects_protected лежит в Task 6 — тест остаётся в Task 6 |
опечатка плана; спека о месте теста молчит | нулевая |
| 2026-10-11 | R-2: T3.9 (транзакции на общем соединении между потоками) — не дефект; в set_note правится только вводящий в заблуждение комментарий; в Task 6 проверить, что API открывает соединение на запрос |
Architecture плана: каждый запрос открывает своё соединение SQLite (WAL, busy_timeout) | если соединение будут делить потоки — ошибки вложенного BEGIN; чинится локом в db.transaction |
| 2026-10-11 | R-3: commands.check_confirmed(conn, cmd, link_types, reporter=None) — необязательный фильтр по reporter для ensure_issue; Task 5 берёт значение из настройки reporter, если она задана |
спека требует «задача с той же темой от нас», а хаб не знает логин агента | без настройки возможно ложное confirmed_by_state чужой задачей с той же темой за 7 дней (только при потерянном отчёте) |
| 2026-10-11 | R-4: временный ключ new:<n> должен быть объявлен ensure_issue раньше по списку пакета (строже спеки «вне пакета») |
иначе зависимая команда с меньшим seq блокирует очередь |
при реальной потребности в обратном порядке — снять проверку и сортировать по зависимостям |
| 2026-10-11 | R-5: db.transaction становится вложенной (если conn.in_transaction — блок входит во внешнюю транзакцию без своих BEGIN/COMMIT) |
после исправления T3.1/T3.2 mirror.rename_note/set_note сами открывают транзакцию, а queue.report_result по плану вызывает их внутри своей — вложенный BEGIN IMMEDIATE падал бы |
вложенный блок не откатывается отдельно от внешнего (savepoint не нужен: ошибка откатывает всю внешнюю транзакцию) |
| 2026-10-11 | R-6: сбой резервной копии в queue.claim не блокирует выдачу команды: исключение ловится, пишется предупреждение в лог |
копия защищает базу хаба и не является условием записи в Jira; иначе команда повисает в claimed на 10 минут |
при сбое диска команда выполнится без свежей копии; строгий вариант — откат claim |
| 2026-10-11 | R-7 (уточняет R-5): вложенный db.transaction использует SAVEPOINT; соединение не делится между потоками |
внутренний блок, чья ошибка перехвачена вызывающим, не должен фиксировать частичные записи | лишние SAVEPOINT на каждый вложенный вызов — дёшево |
| 2026-10-11 | R-8: T6.16 считается закрытой по формам тела; недостижимая ветка except ValueError в h_settings_put и широкое имя теста — minor, правятся в финальной волне |
protected-ключи отсекаются предпроверкой, settings.set_ значения не валидирует |
введённое в заблуждение имя теста; ввод невалидных значений настроек (T6.22) остаётся возможным |
| 2026-10-11 | R-9: повторный импорт CSV передаёт в set_note только непустые значения; пустая ячейка не затирает существующее поле заметки |
иначе повторный запуск jira-hub import молча стирает правки владельца, внесённые в хабе |
поле заметки нельзя очистить повторным импортом (очистка — через notes_set) |
| 2026-10-11 | R-10: зависимость mcp>=1.2,<2 (закреплена исполнителем Task 8) |
uv ставит mcp 2.3.0, где mcp.server.fastmcp удалён (FastMCP стал MCPServer); код плана написан под FastMCP |
позже нужна миграция на MCPServer; пока ограничение исключает неожиданный ломающий апгрейд |
| 2026-10-11 | R-11 (заменяет R-3): ensure_issue не подтверждается по состоянию, пока автор («мы») неизвестен — check_confirmed возвращает (False, None) при reporter is None; настройка reporter не вводится |
логин Jira в настройках хаба запрещён спекой data-model; без фильтра по автору подтверждение находило чужую задачу с той же темой (запись в чужую задачу Jira) | потерянный отчёт по ensure_issue не лечится сверкой, только восстановлением агента по журналу; follow-up: агент передаёт jira_user в claim |
| 2026-10-11 | R-12: инструменты ожидания MCP (batch_wait, refresh) — async, ожидание через asyncio.sleep, HTTP через asyncio.to_thread; refresh по умолчанию wait=False |
синхронный time.sleep до 60 минут блокирует event loop FastMCP (отмена, ping) |
сменилось умолчание refresh в MCP |
| 2026-10-11 | R-13: в выдаче команда, ждущая stale ensure_issue (claimed старше 10 минут), пропускается вместе с остатком своего пакета, выдача идёт к следующему пакету |
иначе один потерянный отчёт останавливает всю очередь, выход только отменой человеком | пакет с зависшим определителем висит до подтверждения или отмены человеком |