20 KiB
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
- Права каталога данных (M-7 #4).
paths.home()иpaths.backups_dir()создают каталог и выставляют0700(os.chmodи для уже существующего).db.connectдля файловой базы —chmod 0700на каталог базы. Файлы внутри не трогаем: закрытого каталога достаточно. Каталог задаётсяJIRA_HUB_HOMEи в тестах лежит вtmp_path— chmod там безвреден. - Слияние заметок (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побеждают. - Статус отменённого пакета (T5.6).
_status_of: если у пакетаcancelled_atи все команды терминальные —cancelled, даже если частьdone. Безcancelled_at— как сейчас. Видимые счётчикиdone/failed/skippedвbatch_viewне меняются. - Обрезка
error(спека commands).report_resultобрезаетerrorдо 1500 символов. Одна точка — для агента и любого клиента. - Поздний отчёт по
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. Ключ для поздних отчётов по прочим типам не нужен — они отбрасываются, как сейчас. - Довыдача
refresh(M-5 #12). При довыдаче командыrefreshхаб обнуляетissues_count,links_countеёsync_run;started_atне меняется (по нему полный прогон помечает соседей — так безопаснее: прочитанное первым выполнением не станет соседом). - Пустой полный прогон (M-12 #4). Пометка соседей по полному прогону делается только при
sync_run.issues_count > 0. Пустой результат валидной JQL почти всегда означает сбой выборки (сменили метку, права), а не «все задачи ушли». - Типы параметров (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. - Миграция под блокировкой (M-2 #12).
migrate: версия читается вне транзакции; если ниже текущей —BEGIN IMMEDIATE(блокировка записи), повторное чтение версии, и только если всё ещё ниже — копия базы и миграция в той же транзакции. Копия делается отдельным соединением к тому же файлу (VACUUM INTOвнутри транзакции SQLite запрещает, а читатель при WAL работает и под чужой блокировкой записи и видит последнее зафиксированное состояние — базу до миграции; проверено 14.10.2026). Второйconnect, дождавшись блокировки, видит новую версию и копию не делает. Docstring исправляется. - Реле (M-3 #8, M-4 #8).
_forwardловитhttp.client.HTTPExceptionвместе сURLError/OSError→ 502. Чисткаwork/— только при первой подготовке каждой папки после старта реле (реле помнит, какую папку уже чистило; новая папка после сменыrelay_dirчистится); повторная подготовка той же папки (пропал подкаталог, не было доступа) создаёт подкаталоги без чистки и пишет «relay: папка подготовлена заново». Запросы, взятые до пропажи папки, обрабатываются своими потоками или пропадают вместе с папкой. - Отмена пакета с взятой командой (M-11 #12).
cancel_batchвозвращает вид пакета с полемstill_claimed— список{id, type}команд пакета вclaimed. MCPbatch_cancelвыводит строку «команда #N уже у агента и будет выполнена; остановить —POST /commands/N/cancelсforce: true». Отдельный инструмент MCP не добавляется. - Подсказка про метки (T3.7). Описание MCP-инструмента
batch_create:ensure_issueдолжна нестиlabelsс базовой меткой (AWPCA), иначе задача окажется внеbase_jqlи выпадет из зеркала. Код не меняется. - Спеки к коду (разбор 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старше интервала; MCPrefresh— id пакета или итог приwait;new:<строка>— только дляnew_tasks.csv, строкиtasks.csv/epics.csvбез ключа пропускаются (T7.3). - Purpose спек (O-4 #8). В каждый
openspec/specs/*/spec.mdвписывается назначение capability (2–3 предложения) прямо в ветке: дельты OpenSpec раздел Purpose не меняют. - Тесты (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):
- CRLF (T3.9). Сравнение описания в
ensure_fieldsи тела комментария вensure_comment— послеreplace("\r\n", "\n")с обеих сторон; отправляется текст команды как есть. Поиск маркера комментария не меняется. - Время журнала в будущем (M-6 #12).
restore_paceпринимает его за «сейчас»:last_call_at = monotonic(), пауза первой команды — полнаяpause_seconds. Лог сохраняется. last_call_iso(M-7 #12).restore_paceзаполняет толькоlast_call_at;last_call_iso— только при настоящем вызове Jira. До первого вызова вclaimуходитnull, хаб (COALESCE) сохраняет прошлое значение.- Ответ папки с неверными типами (M-2 #8).
statusне целое илиbodyне строка — как нечитаемый JSON: перечитать до 2 с, затемConnectionError. pruneпод блокировкой (T2.3).pruneберёт ту жеacquire_lock; если занята — «агент запущен, prune невозможен», код 1. README: снять «Блокировки нет».- Версия агента 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
- Хаб: 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 (миграции нет). - Агент: остановить v3 (Ctrl-C), положить
agent.pyv4 вRSHB/(корень общей папки), на виртуалке скопировать в~/jira-agent-bundle,check,run;agent_status— версия 4. - Откат:
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 | нет |