146 lines
20 KiB
Markdown
146 lines
20 KiB
Markdown
## 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`):
|
||
|
||
16. **CRLF (T3.9).** Сравнение описания в `ensure_fields` и тела комментария в `ensure_comment` — после
|
||
`replace("\r\n", "\n")` с обеих сторон; отправляется текст команды как есть. Поиск маркера комментария не меняется.
|
||
17. **Время журнала в будущем (M-6 #12).** `restore_pace` принимает его за «сейчас»: `last_call_at = monotonic()`, пауза
|
||
первой команды — полная `pause_seconds`. Лог сохраняется.
|
||
18. **`last_call_iso` (M-7 #12).** `restore_pace` заполняет только `last_call_at`; `last_call_iso` — только при настоящем
|
||
вызове Jira. До первого вызова в `claim` уходит `null`, хаб (`COALESCE`) сохраняет прошлое значение.
|
||
19. **Ответ папки с неверными типами (M-2 #8).** `status` не целое или `body` не строка — как нечитаемый JSON:
|
||
перечитать до 2 с, затем `ConnectionError`.
|
||
20. **`prune` под блокировкой (T2.3).** `prune` берёт ту же `acquire_lock`; если занята — «агент запущен, prune невозможен»,
|
||
код 1. README: снять «Блокировки нет».
|
||
21. **Версия агента 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 | нет |
|