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

146 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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 | нет |