2026-10-19 20:31:19 +04:00

37 KiB
Raw Blame History

Context

main eb485d7. Устройство, на которое опираются правки:

  • Настройки — src/jira_hub/settings.py: DEFAULTS содержит base_jql = "project = AURORA AND labels = AWPCA", project_key = "AURORA", board_id = None; AGENT_KEYS отдаёт агенту base_jql и board_id. На живом хабе board_id = 4216, settings_version = 10, schema_version = 3. Ключи labels, comment_marker хаб и агент не читают (наследие jira-sync), здесь не трогаются.
  • Полный прогон — api (POST /refresh {full: true}) и queue.maybe_auto_refresh ставят refresh {jql: base_jql}; queue.is_full_refresh (по params.jql == base_jql) решает, считать ли загрузку полной (authoritative в mirror.apply_state → пересчёт neighbor). mirror.board(conn, base_jql) ищет last_sync по sync_run.query = base_jql.
  • Снимок доски и список — mirror.board отдаёт всё зеркало; mirror.list_issues фильтрует по label через json_each(i.labels); признак neighbor — колонка зеркала (вне базовой JQL).
  • Выдача команд — queue.deliver_params дописывает параметры при выдаче (сейчас size у ensure_attachment), в базе params без них.
  • Агент (jira-agent/src/agent.py, v11): sprints() читает активные и будущие спринты доски settings.board_id; sprint_names(keys) в refresh собирает имена спринтов задач по этой доске; do_ensure_sprint без board_id — failed. Канбан-доска на GET /board/{id}/sprint отвечает Jira ошибкой (спринтов у неё нет).
  • MCP — mcp_server.py, user scope, uv run --project ~/projects/jira-bridge/jira-hub jira-hub mcp; ходит только через client. Описание batch_create требует метку AWPCA в ensure_issue.
  • Веб-приложение — shell/Layout.tsx (шапка «Хаб Jira», вкладки «Доска», «Задачи»), доска — board/useBoardSource.ts (GET /board), список — issues/IssuesPage.tsx (GET /issues, варианты фильтров из снимка доски). Раскладка доски — board/storage.ts, ключи localStorage graph-<имя>… от имени источника (у хаба — «Хаб»). fromHub.ts ставит hasSprint: true всегда.

Goals / Non-Goals

Goals: несколько проектов-меток в одном мосте; у каждого своя доска Jira (scrum или kanban); сессия Claude и страница веб-приложения работают в разрезе одного проекта; AWPCA после выкатки работает как раньше.

Non-Goals и Anti-Goals: см. proposal.md.

Decisions

  1. Проект — таблица project, схема v4. Поля: label (TEXT PK — метка Jira, она же ключ проекта), name (TEXT, непустое), board_id (INTEGER или NULL), board_type (scrum | kanban | NULL — ещё не известен), created_at. Отдельная таблица, а не ключ настроек: проект — сущность со своим жизненным циклом, а настройки уходят агенту целиком и поднимают settings_version при каждой правке. Создание проекта settings_version не поднимает: агенту проекты не нужны, доски он получает в параметрах команд (решение 7).
  2. Метка проекта — непустая строка без пробельных символов, запятых, кавычек и скобок (метка Jira не может содержать пробел; остальное исключено, чтобы метку можно было без экранирования вставить в JQL labels in (…)), до 100 символов. Регистр сохраняется как введён; совпадение с существующей без учёта регистра — 409 (Jira сравнивает метки без учёта регистра в JQL). Название — непустое после обрезки пробелов, до 100 символов. board_id — целое > 0 или отсутствует.
  3. Базовая JQL собирается, а не хранится. base_jql = "project = <project_key> AND labels in (<метки по created_at, затем label>)"; функция в settings (или новом модуле projects), её используют POST /refresh {full}, автообновление, is_full_refresh, mirror.board и GET /settings (поле base_jql в ответе — вычисленное, только для чтения; PUT /settings с base_jql или board_id — 422 с указанием, что JQL собирается из проектов, а доска — свойство проекта). Без проектов base_jql = null: POST /refresh {full} — 422 «нет проектов», автообновление не ставится. Решение владельца: одна JQL на все проекты.
  4. Миграция v3 → v4 (в одной транзакции с копией базы, как прежние): создать project и колонку batch.project; если base_jql в настройках имеет вид … labels = <M> или … labels in (<M1>, …) — завести проекты с этими метками; название проекта AWPCA — «АРМ КА», остальных — сама метка; board_id из настроек достаётся проекту, если проект один, иначе никому (с предупреждением в лог); board_type = NULL (определит первый refresh). Строкам sync_run с query = старая base_jql записать query = новая вычисленная — иначе доска до первого полного прогона показывала бы «полного прогона не было»; так же переписать params.jql команд refresh в queued и claimed, равный старой base_jql, — иначе такой прогон не признается полным. Ключи base_jql и board_id в setting миграция не удаляет: базу открывает любой процесс новой версии (HubClient.from_home в MCP, CLI), и это может случиться после git pull, но до перезапуска старого хаба, а старый хаб эти ключи читает. Новый код base_jql из setting не читает (вычисляет из проектов), а board_id остаётся в AGENT_KEYS как запасной для агента: по нему агент v12 выполнит ensure_sprint, поставленные до миграции без board_id. Ключи убираются отдельной задачей после выкатки (follow-up). В DEFAULTS новой версии ключей base_jql и board_id нет — на новой базе их не появляется. Если base_jql не разобрана — проектов нет, в лог — предупреждение с прежним значением. jira-hub init --config заводит проект AWPCA «АРМ КА» с board_id из конфига вместо записи base_jql/board_id.
  5. Разрез по проекту — одна функция зеркала для доски и списка. Для проекта P: свои — задачи зеркала с меткой P; соседи — задачи без метки P, связанные со своими связью (в любую сторону), и эпики своих (epic_key), если они есть в зеркале. GET /board?project=P отдаёт своих и соседей, neighbor = false у своих и true у соседей; links — связи, у которых хотя бы один конец свой. GET /issues?project=P — та же выборка, фильтр neighbor относится к признаку относительно P; остальные фильтры — поверх. Без project — прежнее поведение (всё зеркало, neighbor из колонки). Неизвестный проект — 404. last_sync — общий (прогон один на все проекты). Доска признак neighbor не читает (fromHub.ts): соседи по связям из других проектов рисуются обычными карточками, как сейчас рисуются соседи зеркала; «+ соседи по связям» доска считает сама по связям. Признак neighbor относительно проекта виден в колонке «Сосед» списка задач. Владелец связей между проектами не ожидает, поэтому отдельной отрисовки соседей-из-другого-проекта нет.
  6. Пакет проекта — проверки в хабе, не в MCP. POST /batches {project: P, …}: неизвестный P — 422. Для каждой команды:
    • ensure_issue — если в labels нет P, хаб дописывает P в конец (параметры хранятся уже с ней); метка другого проекта в labels — 422;
    • ensure_fields — labels_remove с P или labels_add с меткой другого проекта — 422;
    • «метка другого проекта» и «P в labels_remove» сравниваются без учёта регистра (Jira в JQL сравнивает метки так же): awpca в пакете STOCKS_DRRK — тоже чужая метка;
    • команды над существующей задачей (ensure_fields, ensure_comment, ensure_status, ensure_sprint, ensure_attachment) — key должен быть временным ключом этого пакета или задачей зеркала с меткой P, иначе 422 « не в проекте P»;
    • ensure_link — хотя бы один из source, target свой (временный ключ пакета или задача с меткой P);
    • refresh — без ограничений. Пакет хранит batch.project = P; GET /batches?project=P — только пакеты проекта. POST /commands/{id}/retry принимает необязательный project: передан и у исходного пакета другой проект (в том числе NULL) — 422 «команда #N не из проекта P»; передан и совпадает — новый пакет с P и проверками; не передан — новый пакет получает проект исходного. MCP в привязанной сессии всегда передаёт свой P, поэтому сессия не может повторить чужую команду или команду пакета без проекта. Без project — прежнее поведение, проверок нет, batch.project = NULL (автообновление, веб-приложение, старые клиенты). Проверки в хабе, а не в MCP: MCP по границам ходит только через client, а правило о метке проекта должно действовать для любого клиента, который назвал проект.
  7. Доски — в параметрах команд, настройки board_id нет. Агенту не нужен список проектов, ему нужны доски:
    • ensure_sprint — при постановке хаб определяет проект задачи (метки задачи в зеркале ∩ проекты; для временного ключа — метки её ensure_issue в пакете) и дописывает в параметры board_id его доски. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта нет доски; доска kanban («у проекта P канбан-доска, спринтов нет»); тип доски ещё не известен («дождитесь обновления из Jira»). sprint: "backlog" на scrum-доске — как раньше. board_id от клиента в ensure_sprint — 422 (его выставляет хаб). board_id хранится в params (видно в истории команд, переживает довыдачу); повтор команды (retry) снимает board_id с параметров до проверки и вычисляет доску заново.
    • refresh — при выдаче (deliver_params, в базе не хранится) хаб дописывает boards: [{id, type}] — все доски проектов без повторов, type — известный тип или null. Агент для досок с type = null запрашивает GET /rest/agile/1.0/board/{id} и возвращает в результате команды boards: {"<id>": "scrum" | "kanban" | null} (null — доска не найдена или нет доступа; причина — в лог агента); хаб записывает известные типы в project. Имена спринтов агент собирает со всех scrum-досок (известных и только что определённых); канбан-доски пропускает. Одна задача в спринтах двух досок (или двух спринтов одной) — побеждает последнее найденное имя, как в v11 (sprint_names перезаписывает). Агент v12 без boards/board_id в параметрах (старый хаб или команда, поставленная до миграции) берёт settings.board_id, как v11 (ключ остаётся до follow-up, решение 4).
  8. MCP: привязка к проекту флагом запуска. jira-hub mcp --project <метка>. Привязка проверяется при каждом вызове (хаб может быть не запущен при старте MCP): метки нет в GET /projects — инструмент отвечает «проект <метка> не заведён в хабе: project_create или исправьте --project в .mcp.json». В привязанной сессии:
    • issues — GET /issues?project=P; issue — любая задача по ключу (явный ключ — осознанный запрос, а карточка соседа нужна при работе со связями);
    • notes_set — только своя задача проекта (метка P; соседи — нет), иначе отказ;
    • batch_create, command_retry — с project = P (проверки решения 6);
    • batches — GET /batches?project=P; batch_status, batch_wait, batch_cancel — по id, без ограничения;
    • refresh — full — общий полный прогон, keys — как раньше. Без привязки инструменты issues, issue, notes_set, batch_create, command_retry, refresh отвечают «сессия не привязана к проекту: …» с подсказкой про --project и projects; остальные (projects, project_create, agent_status, sync_log, settings_get, settings_set, batches, batch_status, batch_wait, batch_cancel) работают. Решение владельца: «только свои»; безопасное поведение без привязки — моё (см. ниже).
  9. project_create в MCP — POST /projects {label, name, board_id?, author: "claude"}; ответ — проект, id пакета обновления и готовый фрагмент .mcp.json: {"mcpServers": {"jira-hub": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "jira-hub", "mcp", "--project", "<метка>"]}}} с инструкцией: положить в корень репозитория проекта (слить с имеющимся .mcp.json) и сделать /mcp → reconnect (одобрить project-scope сервер). Запись jira-hub в project scope перекрывает одноимённую из user scope. Корень хаба MCP вычисляет от расположения своего пакета (src/jira_hub/ → корень репозитория), не зашивая путь; путь к .venv в подсказку не попадает (uv run восстановить из sys.argv нельзя). Если MCP запущен из worktree, в подсказке окажется путь worktree — это видно в ответе, рабочий MCP запускается из основного checkout. projects — таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии отмечает текущий проект.
  10. POST /projects — {label, name, board_id?, author?} → 201 {project, batch_id}; проект и пакет «Новый проект <метка>» из одного полного refresh ставятся в одной транзакции; автор пакета — author (по умолчанию api). Ошибки: 422 — неверные поля (решение 2), 409 — метка уже есть. GET /projects → список {label, name, board_id, board_type, created_at, issues_count} по created_at, затем label; issues_count — задачи зеркала с меткой проекта.
  11. Веб-приложение — проект в адресе и в памяти браузера. Выбор проекта — выпадающий список рядом с «Хаб Jira» (вариант A): «<метка> — <название>», текущий отмечен; в конце — «+ Новый проект…». Текущий проект — параметр project в адресе /app/board и /app/issues; без него — последний выбранный (localStorage), иначе первый из GET /projects; выбранный записывается в адрес (replace). Неизвестный проект в адресе — сообщение «проект <метка> не найден» со списком проектов. Нет ни одного проекта — доска и список показывают приглашение создать проект. Переходы «Доска» / «Задачи» сохраняют проект. Карточка задачи (/app/issues/{key}) от проекта не зависит. Форма «Новый проект» — окно поверх страницы: метка, название, номер доски (необязательно); ошибка хаба (detail) — рядом с кнопкой «Создать»; после создания — переключение на новый проект. Пакет создания ставится с автором viewer, поэтому доска подхватывает его как свой refresh (правило «Источник данных доски») и показывает ход.
  12. Доска в разрезе проекта. GET /board?project=P; «Обновить из Jira» — как раньше POST /refresh {full: true} — обновляет все проекты (решение владельца). Состояние доски в localStorage — отдельно на проект. Сейчас в web/src/board/storage.ts ключи от имени источника не зависят: fileKey(n) = graph-<n>:board (pos, posdate, lanes, fold, sprint, who, colw) и pref(n) = graph-<n> (флаги neighbors, onlylinked, crit, bydate, hidekind, hideedge, snap, tbl, prob). На проект делятся оба набора: у проекта AWPCA — ровно нынешние ключи, у остальных — те же ключи с суффиксом @<метка> (graph-pos:board@STOCKS_DRRK, graph-crit@STOCKS_DRRK); общие для всего приложения настройки вне доски (если есть) не делятся. Загрузка CSV/JSON и applyJsonSettings пишут в ключи текущего проекта. Так раскладка AWPCA переживает выкатку, а проекты не смешиваются. Фильтр спринтов: hasSprint в модели из хаба — есть ли хотя бы одна своя задача (neighbor = false) со спринтом (даже если канбан-проект связан с задачами scrum-проекта); у канбан-проекта фильтра спринтов нет («колонки «Спринт» нет — показаны все задачи», как у CSV без спринтов).
  13. Список задач в разрезе проекта. GET /issues?project=P&…; варианты фильтров — из снимка GET /board?project=P; параметр project в адресе — рядом с фильтрами; фильтр «Метка» остаётся.
  14. Порядок выкатки. (1) Агент v12 опубликован и agent_status показывает v12 — он понимает и boards/board_id в параметрах, и прежний settings.board_id. (2) Слияние хаба, make web, перезапуск хаба (владелец) — миграция v4. (3) min_agent_version = 12. (4) В awp-ca положить .mcp.json с --project AWPCA и переподключить MCP — до этого сессии awp-ca с задачами работать не могут (решение 8). Хаб новее агента v11 до шага (1) не выкатывать: v11 не знает boards и board_id в параметрах и читает спринты только с доски из настроек — спринты задач других проектов пропадут из зеркала, а ensure_sprint для них уйдёт не на ту доску. Миграция v4 нестрашна для старого хаба, если базу откроет новый MCP/CLI раньше перезапуска (решение 4), но между git pull и перезапуском хаба лучше не открывать новые сессии MCP.
  15. Агент v12 — отдельные issue, ветка, design-only change и PR в jira-agent. Контракт (параметры boards, board_id, результат boards) — в этом change (спека agent живёт в хабе); у агента — решения реализации. Агент сливается и публикуется первым (решение 14).

Risks / Trade-offs

  • Задача с метками двух проектов видна в обоих; ensure_sprint для неё при разных досках — 422. Осознанно: владелец связей между проектами не ожидает.
  • Задача проекта без метки проекта (метку сняли в Jira руками) выпадает из полной JQL и остаётся в зеркале со старыми данными, как сейчас.
  • Без .mcp.json в awp-ca сессии там после выкатки не видят задач — это намеренно (анти-цель «не та метка»), но шаг выкатки легко забыть; он в чеклисте выкатки PR.
  • refresh {keys} хвостом пакета тоже получает boards и тратит по одному вызову GET /board/{id} на доску с неизвестным типом — только до первого определения.
  • Ошибка в номере доски при создании или доска, понадобившаяся канбан-проекту позже, интерфейсом не исправляются (правки проекта нет). Решение владельца 19.10.2026: на первое время он правит базу сам — UPDATE project SET board_id = <номер>, board_type = NULL WHERE label = '<метка>'; тип доски определит следующий refresh. Агенты (Claude) базу хаба напрямую не правят. Доска, на которую агент ответил «не найдена» (null), остаётся с board_type = NULL: каждый refresh снова запрашивает её тип, ensure_sprint отвечает «тип доски ещё не известен».
  • Создание проекта меняет базовую JQL, поэтому last_sync доски становится null («полного прогона не было»), пока не завершится пакет «Новый проект» — он ставится сразу, доска показывает его ход. Переписывать sync_run.query здесь было бы неправдой: прежний прогон новую метку не покрывал.
  • Сравнение меток без учёта регистра (решение 2) при создании; в разрезе проекта (решение 5) метка сравнивается точно, как в зеркале (json_each). Jira хранит метку так, как её записали, — расхождение возможно только при ручной правке метки в Jira другим регистром.

Decisions made without discussion

  1. 2026-10-19 — проект — таблица project, а не ключ настроек. Обоснование: сущность со своим циклом; правка не должна поднимать settings_version и перечитывание настроек агентом. Цена ошибки: таблица и миграция; перенос в настройки — одна миграция назад.
  2. 2026-10-19 — сессия MCP без --project не читает и не пишет задачи. Обоснование: анти-цель — задача с меткой не того проекта; владелец согласился с этим в обсуждении («все ок»), сам режим «только служебные инструменты» — моё уточнение. Цена ошибки: awp-ca до своего .mcp.json не работает с задачами; смягчить — умолчание по проекту.
  3. 2026-10-19 — проверки метки и принадлежности задачи проекту — в хабе (POST /batches {project}), а не в MCP. Обоснование: граница «MCP только через client», правило нужно и будущим клиентам. Цена ошибки: поле пакета и проверки в queue; перенос в MCP — без изменения базы.
  4. 2026-10-19 — хаб дописывает метку проекта в ensure_issue сам (в пакете с project). Прежнее правило «хаб метки сам не добавляет» остаётся для пакетов без проекта. Обоснование: Claude не должен помнить метку — это и есть анти-цель. Цена ошибки: одна ветка в проверках постановки.
  5. 2026-10-19 — issue в MCP открывает любую задачу по ключу, issues — только проект. Обоснование: явный ключ — осознанный запрос, карточка соседа нужна при работе со связями. Цена ошибки: одна проверка в MCP.
  6. 2026-10-19 — доски передаются агенту в параметрах команд (ensure_sprint.board_id при постановке, refresh.boards при выдаче), тип доски определяет агент в refresh. Обоснование: хаб не ходит в Jira; отдельная команда «узнать доску» потребовала бы нового типа команды. Цена ошибки: два параметра контракта.
  7. 2026-10-19 — тип доски неизвестен → ensure_sprint 422, а не попытка на удачу. Обоснование: создание проекта само ставит refresh, окно короткое. Цена ошибки: одна ошибка постановки сразу после создания проекта.
  8. 2026-10-19 — разрез по проекту: свои + соседи по связям + эпики своих; neighbor относительно проекта. Обоснование: логика доски и «+ соседи» уже построена на neighbor; эпики нужны доске для имён. Цена ошибки: функция выборки и тесты к ней.
  9. 2026-10-19 — миграция называет проект AWPCA «АРМ КА», а board_id из настроек отдаёт ему. Обоснование: так называется проект в workspace; доска 4216 — его. Цена ошибки: название правится будущим «переименованием» или вручную.
  10. 2026-10-19 — раскладка доски AWPCA остаётся на прежних ключах localStorage, остальные проекты — на ключах с меткой. Обоснование: анти-цель «AWPCA потерял раскладку». Цена ошибки: особый случай в одном месте storage.ts.
  11. 2026-10-19 — project_create отвечает готовым .mcp.json с командой запуска текущего процесса MCP. Обоснование: владелец хочет, чтобы Claude сам прописал проект в репозитории; путь хаба не зашивается. Цена ошибки: формат подсказки.
  12. 2026-10-19 — метка проекта без пробелов, запятых, кавычек, скобок; уникальность без учёта регистра. Обоснование: безопасная подстановка в JQL. Цена ошибки: отказ на экзотической метке.
  13. 2026-10-19 — GET /batches?project= и batches в привязанной сессии — только пакеты проекта; batch_status/wait/cancel по id — без ограничения. Обоснование: список не засоряется чужими пакетами, а явный id — осознанный запрос. Цена ошибки: фильтр списка.
  14. 2026-10-19 — миграция v4 не удаляет base_jql и board_id из setting; board_id остаётся запасной доской агента; удаление — follow-up. Обоснование: базу мигрирует любой процесс новой версии (MCP, CLI), возможно — под работающим старым хабом (находка builder check). Цена ошибки: два мёртвых ключа до follow-up.
  15. 2026-10-19 — миграция переписывает params.jql у refresh в queued/claimed со старой базовой JQL. Обоснование: иначе прогон из очереди не признается полным. Цена ошибки: одна строка миграции.
  16. 2026-10-19 — retry с project отказывает, если проект исходного пакета другой; хаб снимает свой board_id при повторе. Обоснование: сессия проекта не должна повторять чужие команды; иначе повтор ensure_sprint падал бы на своей же проверке «board_id от клиента». Цена ошибки: поле тела retry и одна ветка.
  17. 2026-10-19 — чужая метка проекта и labels_remove P — без учёта регистра; принадлежность задачи — точно. Обоснование: Jira в JQL сравнивает метки без учёта регистра — awpca попала бы в общую JQL, но ни в чей разрез. Цена ошибки: сравнение в проверках.
  18. 2026-10-19 — notes_set в привязанной сессии — только свои задачи, без соседей. Обоснование: «только свои» владельца. Цена ошибки: одна проверка.
  19. 2026-10-19 — фрагмент .mcp.json — uv run --project <корень хаба от расположения пакета>. Обоснование: вызов uv run из sys.argv не восстановить, путь к .venv в чужом репозитории хрупок. Цена ошибки: формат подсказки.
  20. 2026-10-19 — на проект делятся и раскладка (fileKey), и флаги доски (pref); AWPCA — на нынешних ключах, остальные — с суффиксом @<метка>. Обоснование: спека требует хранить фильтры отдельно; анти-цель — потеря раскладки AWPCA. Цена ошибки: схема ключей в storage.ts.
  21. 2026-10-19 — last_sync после создания проекта — null до завершения его обновления, sync_run.query не переписывается. Обоснование: прежний прогон новую метку не покрывал. Цена ошибки: временная надпись на доске.
  22. 2026-10-19 — пустой author в POST /projects — как api, нестрока — 422 bad_project. Обоснование: как у остальных маршрутов (body.get("author") or "api"); спека требует 422 для не-строки. Цена ошибки: пустая строка пишется автором api, а не отвергается.
  23. 2026-10-19 — CommandError из постановки refresh в POST /projects — 422 bad_batch, а не bad_project. Обоснование: ошибка относится к пакету, а не к полям проекта; спека называет только project_exists и bad_project. Цена ошибки: код ошибки в ответе.