2026-10-14 13:14:36 +04:00

20 KiB
Raw Blame History

Context

Мост в работе с 13.10.2026: хаб на Mac mini (launchd, ~/.local/share/jira-hub/hub.sqlite, схема v2, 196 задач, 11 пакетов), агент v3 на виртуалке через общую папку. Код: хаб main c6aa2ed, агент main d7cd050.

Источник состава — разбор пакета D 14.10.2026 (комментарии в hub#4 и agent#4) и issues hub#8, hub#12, agent#8, agent#12. Маркеры находок сохраняются как в исходных issues; одинаковые номера из разных issue различаются по источнику: «M-3 #4» — rename_note (PR #2 bridge-core), «M-3 #8» — 502 в реле, «M-3 #12» — нестроковый summary.

Найдено при чтении кода:

  • paths.home() и db.connect создают каталоги с правами по umask (на Mac mini — 0755), файлы — 0644.
  • mirror.rename_note делает DELETE … WHERE key = new_key, затем UPDATE key = new_key.
  • queue._status_of даёт cancelled только если все команды cancelled.
  • queue.report_result пишет error как пришёл; ветка позднего отчёта есть только для cancelled.
  • queue._redeliver переводит команду в failed по пределу, не трогая sync_run; повторная выдача refresh не трогает sync_run, а record_state прибавляет счётчики.
  • db.migrate делает копию до транзакции миграции; версию перечитывает уже внутри.
  • relay._prepare вызывается при каждом _ready = False (смена relay_dir, пропажа подкаталога), а чистит work/ как при старте.
  • агент: restore_pace пишет оценку из журнала и в last_call_at, и в last_call_iso; второе уходит в claim.

Goals / Non-Goals

Goals: закрыть пункты пакета C без новых возможностей; openspec validate --specs --strict зелёный; хаб — без миграции схемы; агент — версия 4, совместим с хабом без изменений контракта.

Non-Goals: см. proposal (живые проверки, «на потом», отклонённое в разборе D).

Decisions

  1. Права каталога данных (M-7 #4). paths.home() и paths.backups_dir() создают каталог и выставляют 0700 (os.chmod и для уже существующего). db.connect для файловой базы — chmod 0700 на каталог базы. Файлы внутри не трогаем: закрытого каталога достаточно. Каталог задаётся JIRA_HUB_HOME и в тестах лежит в tmp_path — chmod там безвреден.
  2. Слияние заметок (M-3 #4). rename_note(old, new): если заметки под new нет — переименование, как сейчас. Если есть — в одной транзакции для каждого поля заметки (fact_status, work_kind, done_text, rest_text, owner_note) берётся значение временной заметки, если оно непустое, иначе значение под new; updated_at/updated_by — от более поздней; временная строка удаляется. Так заметка, которую владелец успел завести на настоящий ключ, не теряется, а заполненные при планировании поля new:N побеждают.
  3. Статус отменённого пакета (T5.6). _status_of: если у пакета cancelled_at и все команды терминальные — cancelled, даже если часть done. Без cancelled_at — как сейчас. Видимые счётчики done/failed/skipped в batch_view не меняются.
  4. Обрезка error (спека commands). report_result обрезает error до 1500 символов. Одна точка — для агента и любого клиента.
  5. Поздний отчёт по failed по пределу (M-4 #12). Ветка позднего отчёта в report_result расширяется: команда ensure_issue в cancelled или failed, отчёт done с result.key, у команды нет result — ключ записывается в result, заметка переносится (rename_note), статус не меняется. Для failed это означает «хаб сдался, агент всё-таки создал»: зависимые команды уже skipped, повтор — новой командой. При переводе в failed по пределу выдач (_redeliver) для refresh хаб SHALL проставить sync_run.finished_at. Ключ для поздних отчётов по прочим типам не нужен — они отбрасываются, как сейчас.
  6. Довыдача refresh (M-5 #12). При довыдаче команды refresh хаб обнуляет issues_count, links_count её sync_run; started_at не меняется (по нему полный прогон помечает соседей — так безопаснее: прочитанное первым выполнением не станет соседом).
  7. Пустой полный прогон (M-12 #4). Пометка соседей по полному прогону делается только при sync_run.issues_count > 0. Пустой результат валидной JQL почти всегда означает сбой выборки (сменили метку, права), а не «все задачи ушли».
  8. Типы параметров (M-3 #12). commands.validate проверяет, что строковые параметры (summary, description, temp_key, key, marker, body, source, target, link_type, sprint, assignee, priority, даты) — строки или null там, где null разрешён; иначе ValueError → 422. target в ensure_status — строка или список строк; labels, labels_add, labels_remove, keys — списки строк; replace — bool.
  9. Миграция под блокировкой (M-2 #12). migrate: версия читается вне транзакции; если ниже текущей — BEGIN IMMEDIATE (блокировка записи), повторное чтение версии, и только если всё ещё ниже — копия базы и миграция в той же транзакции. Копия делается отдельным соединением к тому же файлу (VACUUM INTO внутри транзакции SQLite запрещает, а читатель при WAL работает и под чужой блокировкой записи и видит последнее зафиксированное состояние — базу до миграции; проверено 14.10.2026). Второй connect, дождавшись блокировки, видит новую версию и копию не делает. Docstring исправляется.
  10. Реле (M-3 #8, M-4 #8). _forward ловит http.client.HTTPException вместе с URLError/OSError → 502. Чистка work/ — только при первой подготовке каждой папки после старта реле (реле помнит, какую папку уже чистило; новая папка после смены relay_dir чистится); повторная подготовка той же папки (пропал подкаталог, не было доступа) создаёт подкаталоги без чистки и пишет «relay: папка подготовлена заново». Запросы, взятые до пропажи папки, обрабатываются своими потоками или пропадают вместе с папкой.
  11. Отмена пакета с взятой командой (M-11 #12). cancel_batch возвращает вид пакета с полем still_claimed — список {id, type} команд пакета в claimed. MCP batch_cancel выводит строку «команда #N уже у агента и будет выполнена; остановить — POST /commands/N/cancel с force: true». Отдельный инструмент MCP не добавляется.
  12. Подсказка про метки (T3.7). Описание MCP-инструмента batch_create: ensure_issue должна нести labels с базовой меткой (AWPCA), иначе задача окажется вне base_jql и выпадет из зеркала. Код не меняется.
  13. Спеки к коду (разбор D). Только текст спек: sprint_names убирается из перечня настроек (спринты агент читает с доски board_id); коды ответов — 400/401/404/409/413/422/500, реле — 502; прочие методы HTTP — 501 без JSON; page/last_page в POST /agent/state — информационные; GET /issues отдаёт поля зеркала с fact_status, work_kind; автообновление — нет queued/claimed и последний sync_run.started_at старше интервала; MCP refresh — id пакета или итог при wait; new:<строка> — только для new_tasks.csv, строки tasks.csv/epics.csv без ключа пропускаются (T7.3).
  14. Purpose спек (O-4 #8). В каждый openspec/specs/*/spec.md вписывается назначение capability (2–3 предложения) прямо в ветке: дельты OpenSpec раздел Purpose не меняют.
  15. Тесты (M-1 #8, M-10 #12). Тест реле при serve --host 127.0.0.2 (или другом конкретном адресе) — реле пересылает на этот адрес. test_fresh_definer_still_blocks_queue переименовывается по смыслу довыдачи. Сквозной тест: база v1 с командой в claimed → connect (миграция, deliveries = 1) → claim отдаёт её снова с deliveries = 2.

Агентские решения (подробно — jira-agent/openspec/changes/bridge-hygiene/design.md):

  1. CRLF (T3.9). Сравнение описания в ensure_fields и тела комментария в ensure_comment — после replace("\r\n", "\n") с обеих сторон; отправляется текст команды как есть. Поиск маркера комментария не меняется.
  2. Время журнала в будущем (M-6 #12). restore_pace принимает его за «сейчас»: last_call_at = monotonic(), пауза первой команды — полная pause_seconds. Лог сохраняется.
  3. last_call_iso (M-7 #12). restore_pace заполняет только last_call_at; last_call_iso — только при настоящем вызове Jira. До первого вызова в claim уходит null, хаб (COALESCE) сохраняет прошлое значение.
  4. Ответ папки с неверными типами (M-2 #8). status не целое или body не строка — как нечитаемый JSON: перечитать до 2 с, затем ConnectionError.
  5. prune под блокировкой (T2.3). prune берёт ту же acquire_lock; если занята — «агент запущен, prune невозможен», код 1. README: снять «Блокировки нет».
  6. Версия агента 4. Поведение сравнения и темпа изменилось; min_agent_version не поднимается.

Risks / Trade-offs

  • [chmod на чужом JIRA_HUB_HOME] → каталог задаёт владелец; при ошибке chmod (не владелец) хаб пишет предупреждение и продолжает.
  • [Слияние заметок выбирает значение временной заметки] → поля new:N заполняются при планировании и свежее; поля, пустые у временной, сохраняются под ключом.
  • [M-12: настоящий «0 задач» в базовой JQL] → соседи не пометятся до следующего непустого прогона; зеркало это переживает.
  • [Миграция в BEGIN IMMEDIATE держит блокировку записи на время копии] → база меньше 1 МБ, копия — доли секунды; миграция бывает раз в версию.
  • [CRLF: Jira хранит \r\n, команда — \n] → после правки агентом в Jira будет \n; это и есть текст команды.

Migration Plan

  1. Хаб: merge → git pull в jira-hub → launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub; проверить stat -f %Lp ~/.local/share/jira-hub = 700, schema_version = 2 (миграции нет).
  2. Агент: остановить v3 (Ctrl-C), положить agent.py v4 в RSHB/ (корень общей папки), на виртуалке скопировать в ~/jira-agent-bundle, check, run; agent_status — версия 4.
  3. Откат: git checkout предыдущего main и перезапуск; агент — вернуть файл v3.

Open Questions

Нет.

Decisions made without discussion

Владелец 14.10.2026 утвердил состав пакета и решения 5 (M-4 #12), 16 (CRLF), 18 (M-7 #12). Остальное — рулинги при написании спеки.

Маркер Дата Решение Обоснование Цена ошибки
R-1 (design #1) 2026-10-14 Права 0700 только на каталоги; при сбое chmod — предупреждение, не остановка закрытый каталог закрывает файлы; хаб под launchd не должен падать из-за прав токены остаются читаемы, если каталог чужой — видно в логе
R-2 (design #2) 2026-10-14 При слиянии заметок побеждает непустое поле временной заметки поля new:N заполняются при планировании и свежее владелец потеряет поле под ключом, если временная заметка заполнена тем же полем — маловероятно
R-3 (design #3) 2026-10-14 Пакет «отменён», если есть cancelled_at и всё терминально, даже при части done после отмены остаток не применён — «завершён» вводит в заблуждение только отображение
R-4 (design #6) 2026-10-14 Довыдача refresh обнуляет счётчики, started_at не трогает started_at — граница пометки соседей; сдвиг пометил бы прочитанное первым выполнением только отображение счётчиков
R-5 (design #7) 2026-10-14 Соседи по полному прогону помечаются только при issues_count > 0 пустая выборка валидной JQL — почти всегда сбой (метка, права) настоящий «0 задач» не пометит соседей до следующего прогона
R-6 (design #8) 2026-10-14 Проверка типов всех параметров команд, не только summary M-3 #12 — частный случай; одна проверка дешевле серии 500 слишком строгая проверка отвергнет допустимый пакет — 422 с именем параметра, видно сразу
R-7 (design #9) 2026-10-14 Копия перед миграцией — под BEGIN IMMEDIATE, после повторной проверки версии, отдельным соединением-читателем одна копия при параллельных connect (M-2 #12) блокировка записи на долю секунды при миграции
R-8 (design #10) 2026-10-14 work/ чистится только при первой подготовке каждой папки после старта реле при повторной подготовке взятые запросы ещё обрабатываются своими потоками (M-4 #8) после пропажи папки в work/ может остаться файл до перезапуска — безвредно
R-9 (design #11) 2026-10-14 Отмена пакета возвращает still_claimed; MCP-инструмент отмены команды не добавляется M-11 #12 — про информирование; force-отмена нужна редко для остановки взятой команды нужен вызов API, не MCP
R-10 (design #13) 2026-10-14 Расхождения спеки и кода из разбора D закрываются правкой спеки, не кода код разумнее текста спеки (спринты с доски, автообновление от sync_run), потребитель один нет
R-11 (design #21) 2026-10-14 Агент — версия 4, min_agent_version не поднимается поведение сравнения и темпа изменилось; v3 продолжает работать нет
R-14 (design #22) 2026-10-14 В fmt_batch MCP строка про команду у агента без типа команды: «команда #N уже у агента и будет выполнена; остановить — POST /commands/N/cancel с force: true» бриф Task 6 («команда #N (тип) …») противоречил тесту и спеке mcp; побеждает спека; тип доступен в JSON still_claimed в тексте MCP не виден тип команды, только номер
R-15 (design #8) 2026-10-14 Спека commands приведена к коду: null допустим у любого необязательного параметра («не задан»; у ensure_fields — очистка), а не только «где параметр можно очистить» проверка типов (R-6) не должна вводить список очищаемых параметров — агент уже трактует null как «не задан»; финальное ревью хаба M-2 null в необязательном параметре ensure_issue (например, labels) создаст задачу без поля — как и пропуск параметра
R-13 (агент) 2026-10-14 Спека agent: ответ папки с body: null или без body — пустое тело, нечитаемый — только body иного типа совместимость с прежним поведением агента; реле всегда пишет строку; финальное ревью агента M-1 нет