spec: bridge-core — мост Jira (хаб, очередь команд, агент, MCP, импорт)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-11 20:45:10 +04:00
parent 5e0e3b2146
commit 3a11c618a6
9 changed files with 455 additions and 0 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-11

View File

@ -0,0 +1,66 @@
## Context
Две машины в одной локальной сети: Mac mini (`192.168.0.106`, Python 3.13, uv, Docker не всегда запущен) и виртуалка
с macOS и системным Python 3.9.6 (`192.168.0.112`, входящих портов нет, `pip3` есть, доступ к Jira банка
`https://sdlc.go.rshbank.ru/jira` по логину/паролю из `config.json`). Общая папка гипервизора — запасной канал.
Исходный код для переноса: `admin/jira-sync/jira_sync.py` (клиент REST v2 + Agile, темп `Pacer`, карта переходов,
исправленная семантика связей: в POST `/issueLink` `inwardIssue` — источник, в GET `outwardIssue` у X — цель X),
`config.example.json` (id полей, типы, карта переходов, типы связей, доска 4216).
## Goals / Non-Goals
Goals: убрать ручной перенос файлов; один источник ключей; журнал и повторяемость изменений; безопасный повтор любой
команды; темп, при котором Jira не отвечает 429; MCP, через который сессия ставит пакеты и читает состояние.
Non-goals — см. proposal.md.
## Decisions
1. **Два репозитория, один change.** Спека и план лежат в `jira-hub`; `jira-agent` ссылается на них из CLAUDE.md.
Агент меняется редко и живёт на другой машине, поэтому отдельный репозиторий без зависимостей от хаба.
2. **Хаб — один процесс: SQLite + HTTP + MCP.** Один писатель в базу, нет зависимости от Docker, база — один файл.
Агент и интерфейсы ходят только через HTTP; виртуалке ничего не ставить, у неё нет входящих портов.
3. **Команды — желаемый результат** (`ensure_*`), каждая на одной задаче, выполняется как «прочитать из Jira → если результат
уже есть, `done` без записи → иначе записать». Повтор безопасен по построению.
4. **Хаб не повторяет команды.** Взятая команда без ответа видна как «нет ответа от агента с …»; повторяет её только сам
агент после перезапуска по своему журналу, и только через проверку. Отменить взятую команду может только человек.
5. **Истина в Jira.** После каждого пакета хаб добавляет `refresh` по затронутым ключам; команда без ответа, результат
которой виден в зеркале, помечается `confirmed_by_state`.
6. **Политика темпа в хабе, пауза на команде.** При выдаче команды хаб ставит «не раньше, чем через N с после предыдущей
операции в Jira» по типу команды и типу предыдущей. Агент помнит только время последнего вызова Jira. Нет следующей
команды — нет паузы. 429/503 — Retry-After в агенте (инструкция сервера, не политика).
7. **Пакет = одно действие пользователя.** Команды пакета — по порядку, пакеты — по порядку постановки, одна команда в полёте.
Упавшая команда не останавливает пакет; команды, ссылающиеся на её результат, — `skipped`.
8. **Временные ключи `new:<n>`** живут в хабе: `ensure_issue` с временным ключом, после `done` хаб подменяет `new:<n>`
в параметрах следующих команд пакета и в `issue_note`. У агента соответствия нет.
9. **Зеркало — JQL из настроек** (`project = AURORA AND labels = AWPCA`); задачи, на которые ведут связи, но вне JQL,
дочитываются по ключу и помечаются `neighbor`.
10. **Служебные заметки отдельно от зеркала** (`issue_note`), чтобы обновление из Jira их не затирало.
11. **Настройки агента приходят от хаба** (id полей, карта переходов, типы связей, политика темпа, версия настроек);
локально у агента — адрес хаба, токен, Jira и учётные данные.
## Risks / Trade-offs
- Чтение перед каждой записью — лишний вызов на команду. Цена мала (пауза перед чтением 2 с), а гарантия идемпотентности
стоит дороже.
- Поиск дубля при `ensure_issue` по точной теме и автору за 7 дней может не найти задачу, если тему успели поправить
в Jira руками. Остаточный риск принят: окно между ответом Jira и записью журнала — миллисекунды.
- Один агент и одна команда в полёте — медленно для сотни операций (как и сейчас: ~4,5 ч на полную актуализацию).
Это цена темпа, не архитектуры.
- Хаб должен быть запущен для MCP; если нет — инструменты возвращают понятную ошибку, не падают.
- SQLite по сети не используется; при переезде хаба на другую машину — переносится файл.
## Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 2026-10-11 | Хаб: SQLite в одном процессе с HTTP, без PostgreSQL | один писатель, Docker здесь не всегда поднят, виртуалке ничего не ставить; владелец отдал плумбинг исполнителю | смена хранилища — замена одного модуля хаба, API не меняется |
| 2026-10-11 | HTTP-библиотека хаба — stdlib `http.server` + `ThreadingHTTPServer`, без FastAPI | API маленький, зависимостей ноль, тесты через `urllib`; MCP — пакет `mcp` (FastMCP) через uv | при росте API — переезд на FastAPI внутри того же репозитория |
| 2026-10-11 | Порт хаба 8790, привязка к `0.0.0.0` | свободный порт; виртуалка в LAN с другим адресом, `127.0.0.1` ей недоступен | пересечение портов — поправить настройку |
| 2026-10-11 | `claim` — долгий запрос до 30 с, затем сразу следующий | минимальная задержка выдачи без постоянного трафика | — |
| 2026-10-11 | Поиск дубля в `ensure_issue`: `project = AURORA AND reporter = currentUser() AND created >= -7d` + точное сравнение темы на клиенте | JQL `summary ~` нечёткий; точное сравнение в агенте надёжнее | дубль при смене темы руками в первые 7 дней; хаб показывает такие команды отдельно |
| 2026-10-11 | Исполнитель входит в `ensure_fields` | 11.10 исполнители ставились руками, в просмотрщике это нужная операция; владелец согласился | — |
| 2026-10-11 | Паузы по умолчанию: изменение задачи 120 с, комментарий и переход 30 с, чтение 2 с, создание связи 30 с | значения `pacing` из `config.example.json`, проверенные на Jira банка 10–11.10 | настройки, правятся без кода |
| 2026-10-11 | Резервная копия файла базы перед каждым пакетом в `backups/`, хранить последние 30 | заменяет историю версий зеркала | место на диске; число в настройках |
| 2026-10-11 | Хранение базы и настроек хаба — `~/.local/share/jira-hub/` | вне git, переживает пересоздание worktree | — |
| 2026-10-11 | Регистрация MCP — в `~/.claude.json` для проекта `awp-ca` (stdio, `uv run jira-hub mcp`) | как остальные MCP этого проекта | — |
| 2026-10-11 | Полная синхронизация по расписанию: хаб ставит `refresh` по базовой JQL раз в 60 мин, если агент на связи и очередь пуста | свежее зеркало без участия человека | лишние чтения; интервал в настройках, 0 отключает |

View File

@ -0,0 +1,39 @@
## Why
Актуализация Jira-бэклога АРМ КА (проект AURORA, метка AWPCA) ведётся из этой машины, а доступ к Jira банка есть только
у виртуалки (`192.168.0.112`). Рабочий цикл 11.10.2026: правка CSV и скрипта здесь → владелец копирует файлы в виртуалку
через общую папку → запускает `jira_sync.py` → выгружает фильтр в CSV → копирует обратно → сверка. За вечер пять кругов,
один закончился дублем задачи (AURORA-1588): ключи созданных задач жили в `new_tasks.csv` виртуалки, перенос в
неправильную сторону создал задачу повторно. Владелец больше не хочет быть курьером, а CSV как источник истины себя
исчерпал.
Решения владельца 11.10.2026 (сессия brainstorming, `admin/jira-sync/next-session-bridge.md`):
- единственный источник истины по задачам проекта — база на этой машине (Mac mini, `192.168.0.106`); виртуалка — клиент;
- интерфейсы к базе — просмотрщик (UI, позже) и MCP (сейчас); CSV и `jira_sync.py` как CLI уходят;
- модель изменений — гибрид: зеркало задач только для чтения, любая правка порождает команду в очереди (журнал, повторяемость);
- темп запросов к Jira — требование первого порядка (429), политика темпа живёт в хабе, не в агенте;
- хаб никогда не повторяет команды сам; каждая команда — желаемый результат, выполняется как «проверь, потом сделай»;
- зеркало ограничено JQL `project = AURORA AND labels = AWPCA` (полный набор задач нашего проекта; AURORA шире), фильтры —
на стороне приложения;
- первая поставка — только ядро (база, хаб, агент, импорт, MCP); просмотрщик на живых данных и правки из UI — отдельные change'и;
- по репозиторию на компонент: `jira-hub` (эта машина) и `jira-agent` (виртуалка), ремоуты — приватные в Gitea под `aep`.
## What Changes
- **jira-hub** (новый репозиторий, Python 3.13, uv): SQLite-база с зеркалом задач, служебными заметками, пакетами и
командами; HTTP API в локальной сети для агента и интерфейсов; MCP-сервер (stdio) как второй вход в тот же код;
разовый импорт CSV-пакета `admin/jira-sync/`.
- **jira-agent** (новый репозиторий, Python 3.9, только stdlib): один файл `agent.py` + `agent.json`; опрашивает хаб,
выполняет команды в Jira по правилу «проверь, потом сделай», ведёт локальный журнал, возвращает состояние задач.
- Спеки: `data-model`, `commands`, `agent`, `hub-api`, `mcp`, `migration` (все новые).
- `admin/jira-sync/README.md` получает пометку «заменено мостом» (правка вне репозиториев, по чеклисту закрытия);
`awp-ca/CLAUDE.md` — две строки в таблице репозиториев.
## Non-goals
- Просмотрщик `sprint-2-links.html` на живых данных и правки из него (отдельные change'и: `viewer-live`, `viewer-edit`).
- Автоматическая подстройка темпа по фактам 429 (хаб только показывает их).
- История версий зеркала (резервные копии файла базы перед пакетом заменяют её).
- Работа с типом «Ошибка» вне базовой JQL, несколько проектов Jira, несколько агентов.
- TLS и аутентификация пользователей в хабе: домашняя сеть, два статических токена.
- Произвольные JQL на запись, произвольные вызовы REST из очереди.

View File

@ -0,0 +1,62 @@
## ADDED Requirements
### Requirement: Состав и настройки агента
Агент SHALL состоять из одного файла `agent.py` (Python ≥ 3.9, только стандартная библиотека) и файла `agent.json`:
`hub_url`, `hub_token`, `jira.base_url`, `jira.auth` (`basic` с `user`/`password` или `bearer` с `token`), `verify_ssl`,
`ca_bundle`, `timeout`, `poll_seconds`. Всё остальное (id полей, типы, карта переходов, типы связей, доска, темп) агент
SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT отправляться в хаб
и MUST NOT попадать в журнал и логи.
#### Scenario: Проверка без записи
- **WHEN** `python3 agent.py check`
- **THEN** агент печатает доступность хаба (версия настроек) и Jira (`GET /myself`), в Jira ничего не пишет
### Requirement: Цикл выполнения
Агент SHALL работать одним процессом: `claim` к хабу (долгий запрос) → при получении команды: досыпать до
`pause_seconds` от последнего вызова Jira → запись «started» в журнал → выполнение по контракту `commands` → запись
«finished» с результатом → отчёт хабу, повторяемый до подтверждения. Одна команда в полёте. При изменении
`settings_version` в ответе `claim` агент SHALL перечитать настройки до выполнения команды.
#### Scenario: Остановка между командами
- **WHEN** получен Ctrl-C во время выполнения команды
- **THEN** текущая команда доделывается и отчёт отправляется, затем процесс завершается
### Requirement: Журнал на диске и восстановление
Агент SHALL вести `journal.jsonl` (только дозапись) рядом с собой: строки `started {command_id, at}`, `finished {command_id,
status, result, error, jira_calls, rate_limited}`, `acked {command_id}`. При старте агент SHALL: отправить хабу все
`finished` без `acked`; для `started` без `finished` — выполнить команду заново по правилу «проверь, потом сделай»
(запросив её параметры у хаба по id). Команда `python3 agent.py prune` SHALL удалять из журнала записи с `acked` старше
30 дней.
#### Scenario: Падение после ответа Jira
- **WHEN** агент упал между `POST /issue` и записью `finished`, затем перезапущен
- **THEN** агент находит в журнале `started` без `finished`, запрашивает команду у хаба, находит созданную задачу поиском
дубля и отчитывается `done` с её ключом; второй задачи нет
### Requirement: Ограничение темпа и 429
Агент SHALL помнить время последнего вызова Jira (любого) и не начинать команду раньше `pause_seconds` после него.
На HTTP 429 и 503 агент SHALL ждать `Retry-After` (иначе 60 с × номер попытки) и повторять тот же запрос до 5 раз,
считая попытки в `rate_limited`; после 5 неудач — `failed`. Пауза между записями внутри одной команды — по зазору типа команды.
#### Scenario: 429 с Retry-After
- **WHEN** Jira ответила 429 с `Retry-After: 90`
- **THEN** агент ждёт 90 с, повторяет тот же запрос; в отчёте `rate_limited = 1`
### Requirement: Чтение состояния
`refresh` SHALL выполняться запросом `search` (`jql`, `fields` = поля зеркала включая `issuelinks` и спринт,
`maxResults = 100`, постранично) и отправляться хабу пачками по 100 задач со связями; ключи из `issuelinks`, которых нет
в выборке и в зеркале хаба (хаб сообщает список известных), агент SHALL дочитать одним `search` по `key in (...)`
и отправить с признаком `neighbor`.
#### Scenario: Полная синхронизация
- **WHEN** `refresh {jql: "project = AURORA AND labels = AWPCA"}` при 180 задачах
- **THEN** 2 вызова `search` + 1 на соседей; `sync_run.issues_count = 180 + соседи`
### Requirement: Доставка и запуск
README агента SHALL описывать: копирование `agent.py` и `agent.example.json` через общую папку, заполнение `agent.json`,
`check`, запуск в терминале и через `nohup` с логом, опциональный launchd-plist, `prune`. Версия агента SHALL быть в
константе файла и в каждом отчёте; хаб предупреждает в `agent_status`, если версия ниже минимальной из настроек.
#### Scenario: Устаревший агент
- **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2`
- **THEN** `agent_status.warning` = «обновите агента», команды выдаются по-прежнему

View File

@ -0,0 +1,81 @@
## ADDED Requirements
### Requirement: Команда — желаемый результат на одной задаче
Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить →
записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done`
без записи. Типы команд и их контракт:
| Тип | Параметры | Что читает | Что пишет |
|---|---|---|---|
| `ensure_issue` | `temp_key` (`new:<n>`), `issue_type` (`task`/`epic`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = <proj> AND reporter = currentUser() AND created >= -7d`, точное сравнение `summary` | при отсутствии — `POST /issue` (для эпика — с `epic_name_field_id`); результат `key` |
| `ensure_fields` | `key`, любое подмножество: `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels_add`, `labels_remove`, `epic_key`, `assignee` | `GET /issue/{key}` | `PUT /issue/{key}` только с отличающимися полями; эпик — по `epic_link.mode` |
| `ensure_comment` | `key`, `marker`, `body` | `GET /issue/{key}/comment` | комментарий с `marker` в начале: `PUT` если текст отличается, `POST` если нет |
| `ensure_status` | `key`, `target` (имя статуса или список-путь) | `GET /issue/{key}` (статус), `GET /issue/{key}/transitions` на каждом шаге | `POST /transitions` по шагам пути; при отсутствии перехода — `failed` с перечнем доступных |
| `ensure_link` | `source`, `target`, `link_type` (логическое имя из `link_types`), `replace` (bool) | `GET /issue/{source}` (`issuelinks`) | нет нужной и нет конфликта — `POST /issueLink`; конфликт (другой тип из `link_types` или обратное направление) и `replace` — `DELETE` лишней, затем `POST`; конфликт без `replace` — `failed` с описанием |
| `ensure_sprint` | `key`, `sprint` (имя или `backlog`) | `GET /issue/{key}` (поле спринта), список спринтов доски из `board_id` (активные и будущие) | `POST /rest/agile/1.0/sprint/{id}/issue` или `POST /rest/agile/1.0/backlog/issue` |
| `refresh` | `keys` (список) или `jql` | `search` с полями зеркала, по 100 на страницу; плюс дочитывание соседей по `issuelinks` | ничего в Jira; состояние уходит в хаб |
Семантика `ensure_link`: `source` блокирует/ссылается на `target`; агент в `POST /issueLink` SHALL ставить `source` в
`inwardIssue`, `target` в `outwardIssue` (проверено на Jira банка 11.10.2026).
#### Scenario: Повтор уже выполненной команды
- **WHEN** `ensure_fields {key: AURORA-1533, duedate: 2026-10-23}` выполняется, а в Jira срок уже 23.10
- **THEN** агент не делает `PUT`, отчёт `done`, `jira_calls = 1`
#### Scenario: Замена связи
- **WHEN** `ensure_link {source: AURORA-1537, target: AURORA-1538, link_type: "связанные", replace: true}` и в Jira между
ними «блокирует начало» 1537→1538
- **THEN** `DELETE` старой связи, пауза по политике, `POST` новой; отчёт `done` с обеими операциями
#### Scenario: Связь-конфликт без replace
- **WHEN** та же команда с `replace: false`
- **THEN** `failed`, в `error` найденная связь, Jira не изменена
#### Scenario: Переход по пути
- **WHEN** `ensure_status {key: X, target: ["Разработка", "Готово"]}` и X в «Ожидание разработки»
- **THEN** два `POST /transitions` с паузой между ними; если X уже «Готово» — `done` без записи
#### Scenario: Создание с проверкой дубля
- **WHEN** `ensure_issue` повторяется после падения агента, а задача с той же темой от нас уже создана час назад
- **THEN** агент не создаёт вторую, отчёт `done` с ключом найденной
### Requirement: Временные ключи и ссылки внутри пакета
Параметр-ключ любой команды MAY быть временным `new:<n>`. Хаб SHALL выдавать агенту такую команду только после `done`
команды `ensure_issue` с тем же `temp_key` в том же пакете, подставив настоящий ключ. Если `ensure_issue` завершилась
`failed` или `cancelled`, зависимые команды SHALL получить `skipped`. Временный ключ вне пакета с его `ensure_issue`
SHALL отвергаться при создании пакета (422).
#### Scenario: Создать и привязать
- **WHEN** пакет: `ensure_issue {temp_key: new:60, …}`, `ensure_link {source: new:60, target: AURORA-1537, …}`
- **THEN** вторая команда выдаётся агенту с `source = AURORA-1590` после завершения первой
### Requirement: Пакет и порядок выполнения
Пакет SHALL создаваться одним вызовом с автором, заголовком и списком команд; хаб SHALL назначить `seq` по порядку списка
и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh`). Очередь SHALL
выдавать команды в порядке `(batch.created_at, seq)`, одну в полёте. Пакет и его `queued` команды MAY быть отменены;
`claimed` команда MUST NOT отменяться автоматически.
#### Scenario: Оценка длительности
- **WHEN** создаётся пакет из 3 `ensure_fields` и 2 `ensure_comment`
- **THEN** `estimate_seconds` = сумма пауз по политике для последовательности типов + пауза `refresh`
### Requirement: Политика темпа и пауза на команде
Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и
`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint` 30, `refresh` 2). При выдаче команды
хаб SHALL сообщить `pause_seconds` = зазор по типу выдаваемой команды; агент SHALL выдержать его от времени своего
последнего вызова Jira. Внутри одной команды с несколькими записями (`ensure_link` с `replace`, путь переходов) агент
SHALL выдерживать зазор типа команды между записями.
#### Scenario: Нет следующей команды — нет паузы
- **WHEN** очередь пуста
- **THEN** `claim` возвращает пустой ответ, агент не спит сверх долгого запроса
### Requirement: Ошибки и подтверждение по состоянию
Ошибка команды SHALL записываться в `error` (HTTP-код, текст Jira до 1500 символов), пакет продолжается. Команда в
`claimed` без ответа SHALL показываться как «нет ответа от агента с <claimed_at>». Если после любого `refresh` желаемый
результат такой команды виден в зеркале, хаб SHALL перевести её в `confirmed_by_state`. Автоматических повторов хаб
MUST NOT делать; повтор — новая команда с теми же параметрами по запросу интерфейса.
#### Scenario: Отчёт потерян, факт на месте
- **WHEN** `ensure_sprint {key: X, sprint: "Спринт 2"}` в `claimed` 20 минут, агент перезапущен, `refresh` показал X в «Спринт 2»
- **THEN** команда `confirmed_by_state`, следующая команда очереди выдаётся

View File

@ -0,0 +1,67 @@
## ADDED Requirements
### Requirement: Схема базы хаба
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_note`, `batch`, `command`,
`sync_run`, `agent_status`, `setting`. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру версии
в `setting`. Денежных и дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени).
#### Scenario: Первый запуск
- **WHEN** хаб стартует с несуществующим файлом базы
- **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая
### Requirement: Зеркало задачи
Таблица `issue` SHALL хранить задачу Jira как есть: `key` (PK), `id`, `issue_type`, `summary`, `description` (wiki),
`status`, `priority`, `assignee`, `reporter`, `duedate`, `planned_start`, `planned_end`, `epic_key`, `labels` (JSON-список),
`sprint`, `jira_updated`, `fetched_at`, `neighbor` (0/1 — вне базовой JQL, дочитана по связи), `created`. Таблица `issue_link`
SHALL хранить связи `(source_key, target_key, link_type)` с направлением по семантике Jira: `source` блокирует/ссылается
на `target` (у `source` в GET это `outwardIssue`). Обновлять `issue` и `issue_link` SHALL только загрузка состояния от
агента; HTTP API интерфейсов и MCP MUST NOT писать в них.
#### Scenario: Загрузка состояния заменяет связи
- **WHEN** агент прислал состояние задачи X с двумя связями
- **THEN** все строки `issue_link` с `source_key = X` заменены на присланные; строки с `target_key = X`, не присланные
со стороны X, не трогаются до загрузки их источника
#### Scenario: Соседняя задача
- **WHEN** в `issuelinks` задачи из базовой JQL есть ключ Y вне JQL
- **THEN** Y сохраняется с `neighbor = 1`; при следующем полном `refresh`, если Y вошла в JQL, `neighbor = 0`
### Requirement: Служебные заметки
Таблица `issue_note` SHALL хранить поля, которых нет в Jira: `key` (PK; ключ Jira или временный `new:<n>`),
`fact_status`, `work_kind`, `done_text`, `rest_text`, `owner_note`, `updated_at`, `updated_by`. Обновление зеркала
MUST NOT менять `issue_note`. При завершении `ensure_issue` с временным ключом хаб SHALL переименовать строку заметок
на настоящий ключ.
#### Scenario: Заметка переживает refresh
- **WHEN** у задачи есть `issue_note.fact_status = "В работе"` и приходит полное обновление зеркала
- **THEN** `fact_status` не изменился
### Requirement: Пакеты и команды
Таблица `batch` SHALL хранить `id`, `title`, `author`, `created_at`, `cancelled_at`, `estimate_seconds`.
Таблица `command` SHALL хранить `id`, `batch_id`, `seq`, `type`, `params` (JSON), `status`
(`queued | claimed | done | failed | skipped | cancelled | confirmed_by_state`), `claimed_at`, `finished_at`, `result` (JSON),
`error`, `jira_calls`, `rate_limited` (число 429/503), `pause_seconds` (выданная пауза), `depends_on` (id команды,
чей результат подставляется). Статус пакета SHALL вычисляться из команд, не храниться.
#### Scenario: Статус пакета
- **WHEN** в пакете 3 команды: `done`, `claimed`, `queued`
- **THEN** статус пакета «выполняется»; когда все в терминальных статусах — «завершён», с числом `failed`/`skipped`
### Requirement: Журнал синхронизаций и статус агента
Таблица `sync_run` SHALL хранить каждую загрузку состояния: `started_at`, `finished_at`, `kind` (`keys | jql`), `query`,
`issues_count`, `links_count`, `jira_calls`, `rate_limited`, `command_id`. Таблица `agent_status` (одна строка) SHALL
хранить `last_seen_at`, `agent_version`, `settings_version`, `current_command_id`, `last_jira_call_at`.
#### Scenario: Агент давно не выходил на связь
- **WHEN** `last_seen_at` старше 5 минут
- **THEN** `agent_status` через API и MCP отдаётся с признаком `offline`
### Requirement: Настройки
Таблица `setting` SHALL хранить парами ключ–JSON: `base_jql`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
`transitions_map`, `pacing`, `board_id`, `sprint_names`, `tokens` (агентский и клиентский), `settings_version`
(увеличивается при любой правке), `backup_keep`, `auto_refresh_minutes`. Начальные значения SHALL импортироваться из
`admin/jira-sync/config.example.json` командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать.
#### Scenario: Правка настроек поднимает версию
- **WHEN** через API изменён `pacing`
- **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию

View File

@ -0,0 +1,70 @@
## ADDED Requirements
### Requirement: Процесс и доступ
`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты SHALL требовать `Authorization: Bearer <token>`:
агентский токен открывает только `/agent/*`, клиентский — всё остальное. Ответы — JSON, ошибки — `{error, detail}` с
кодами 400/401/404/409/422.
#### Scenario: Чужой токен
- **WHEN** агентским токеном вызван `GET /issues`
- **THEN** 401
### Requirement: Маршруты агента
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
- `POST /agent/claim` `{agent_version, settings_version, last_jira_call_at}` — долгий запрос до 30 с; ответ `{command:
{id, type, params, pause_seconds}, settings_version}` или `{command: null, settings_version}`; обновляет `agent_status`.
- `GET /agent/settings` — настройки для агента (без токенов).
- `GET /agent/commands/{id}` — команда по id (для восстановления из журнала).
- `POST /agent/commands/{id}/result` `{status, result, error, jira_calls, rate_limited}` — идемпотентно: повторный
отчёт по команде в терминальном статусе отвечает 200 без изменений.
- `POST /agent/state` `{command_id, issues: [...], links: [...], page, last_page}` — загрузка состояния пачками;
хаб обновляет зеркало и `sync_run`.
- `GET /agent/known-keys` — ключи зеркала (для определения соседей).
#### Scenario: Выдача с подстановкой ключа
- **WHEN** следующая команда ссылается на `new:60`, а `ensure_issue` для `new:60` завершилась с `AURORA-1590`
- **THEN** `claim` отдаёт команду с `AURORA-1590` в параметрах
#### Scenario: Повторный отчёт
- **WHEN** агент дважды прислал `result` для команды 15
- **THEN** второй ответ 200, статус и результат не изменились
### Requirement: Маршруты интерфейсов
Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном:
- `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`,
`q` (подстрока темы), `limit`/`offset` — компактные карточки (ключ, тема, статус, приоритет, срок, исполнитель, спринт,
эпик, заметки кратко).
- `GET /issues/{key}` — задача целиком: поля, связи в обе стороны, заметки, последние 10 команд по ней.
- `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела).
- `POST /batches` `{title, author, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, неверных
параметрах, временном ключе без `ensure_issue`.
- `GET /batches?limit=`, `GET /batches/{id}` (с командами), `POST /batches/{id}/cancel`.
- `POST /commands/{id}/retry` — новая команда с теми же параметрами в новом пакете «Повтор команды N».
- `POST /commands/{id}/cancel` — только для `queued`; для `claimed` — 409, если не передан `force: true`.
- `POST /refresh` `{keys | jql | full: true}` → пакет из одного `refresh`.
- `GET /agent`, `GET /sync-runs?limit=`, `GET /settings`, `PUT /settings` (частично; поднимает `settings_version`).
#### Scenario: Фильтр по спринту
- **WHEN** `GET /issues?sprint=Спринт 2`
- **THEN** только задачи с таким `sprint`, отсортированы по сроку, затем по ключу
#### Scenario: Отмена взятой команды
- **WHEN** `POST /commands/15/cancel` без `force`, команда `claimed`
- **THEN** 409 с `claimed_at`; с `force: true` — `cancelled`, следующий отчёт агента по ней принимается, но статус не меняется
### Requirement: Резервные копии
Перед выдачей первой команды пакета, содержащего записи в Jira, хаб SHALL скопировать файл базы в `backups/<дата-время>-<batch>.sqlite`
и хранить последние `backup_keep` копий.
#### Scenario: Ротация
- **WHEN** копий больше `backup_keep`
- **THEN** самые старые удаляются до `backup_keep`
### Requirement: Автообновление зеркала
Если `auto_refresh_minutes > 0`, агент на связи и очередь пуста дольше этого интервала, хаб SHALL поставить пакет
«Автообновление» из `refresh {jql: base_jql}`.
#### Scenario: Очередь занята
- **WHEN** интервал прошёл, но в очереди есть `queued` команды
- **THEN** автообновление не ставится до опустошения очереди

View File

@ -0,0 +1,36 @@
## ADDED Requirements
### Requirement: Сервер и регистрация
`jira-hub mcp` SHALL запускать MCP-сервер по stdio (пакет `mcp`), ходящий в HTTP API хаба клиентским токеном из
настроек. Регистрация — в `~/.claude.json` для проекта `awp-ca` как `jira-hub` (`uv run --project <путь> jira-hub mcp`).
Если хаб недоступен, каждый инструмент SHALL вернуть текст «хаб не запущен: jira-hub serve», а не исключение.
#### Scenario: Хаб выключен
- **WHEN** вызван `issues` при остановленном хабе
- **THEN** ответ инструмента — одна строка с подсказкой, сервер MCP продолжает работать
### Requirement: Инструменты
| Инструмент | Параметры | Результат |
|---|---|---|
| `issues` | фильтры как у `GET /issues`, `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды |
| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки |
| `batch_create` | `title`, `commands` (список объектов по контракту `commands`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации |
| `batch_status` | `id` | статусы команд, ошибки, оценка остатка |
| `batch_wait` | `id`, `timeout_minutes` (≤ 60) | как `batch_status`, возвращается по завершении или по таймауту |
| `batches` | `limit` | последние пакеты со статусом |
| `command_retry`, `batch_cancel` | `id` | новый пакет / результат отмены |
| `refresh` | `keys` или `full`, `wait` (bool) | `sync_run` или id пакета |
| `agent_status` | — | онлайн/офлайн, версия, текущая команда, 429 за сутки, версия настроек |
| `sync_log` | `limit` | последние `sync_run` |
| `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` |
Автор команд из MCP SHALL быть `claude`; из API без автора — `api`.
#### Scenario: Пакет из чата
- **WHEN** `batch_create {title: "1538 в спринт 2", commands: [ensure_fields{…}, ensure_link{…}, ensure_sprint{…}]}`
- **THEN** пакет создан с четырьмя командами (три + `refresh`), ответ содержит оценку в минутах
#### Scenario: Ожидание результата
- **WHEN** `batch_wait {id, timeout_minutes: 10}` и пакет завершился через 6 минут
- **THEN** ответ через 6 минут со статусами всех команд

View File

@ -0,0 +1,32 @@
## ADDED Requirements
### Requirement: Инициализация настроек
`jira-hub init --config <admin/jira-sync/config.example.json>` SHALL перенести в `setting`: `project_key`, типы задач,
`epic_name_field_id`, `fields`, `epic_link`, `priority_map`, `labels`, `status_labels`, `kind_labels`, `comment_marker`,
`pacing` (в новую форму по типам команд), `transitions.map`, `link_types`, `sprints.board_id`; сгенерировать два токена;
установить `base_jql = "project = AURORA AND labels = AWPCA"`. Поля `auth`, `base_url`, `verify_ssl`, `ca_bundle`
MUST NOT переноситься (они у агента).
#### Scenario: Повторный init
- **WHEN** `init` вызван на уже инициализированной базе
- **THEN** настройки не перезаписываются без `--force`, токены сохраняются
### Requirement: Импорт CSV-пакета
`jira-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.csv` и заполнить
`issue_note` (ключ Jira; для строк без ключа — `new:<номер строки>`): статус по факту, вид работы, что сделано, что
осталось, внутренние колонки. `links.csv` SHALL импортироваться как сверочный набор в отчёт импорта (ожидаемые связи
против зеркала после первого `refresh`), не как команды. Импорт идемпотентен: повтор обновляет заметки, не дублирует.
#### Scenario: Импорт и первая синхронизация
- **WHEN** выполнены `init`, `import`, запущены хаб и агент, `refresh full` завершён
- **THEN** число задач зеркала ≥ числа строк `tasks.csv` + созданных из `new_tasks.csv`; отчёт импорта перечисляет ключи
из CSV, которых нет в зеркале, и связи из `links.csv`, которых нет в `issue_link`
### Requirement: Что остаётся в admin/
После ввода моста `admin/jira-sync/README.md` SHALL получить в начале пометку «заменено мостом jira-hub/jira-agent,
CSV заморожены <дата>»; `jira_sync.py`, CSV, `runs/`, `summary.md`, `_generator/` остаются как история без изменений.
Артефакт просмотрщика на claude.ai MUST NOT переопубликовываться.
#### Scenario: Пометка в README
- **WHEN** мост запущен и первая синхронизация сверена с CSV
- **THEN** README пакета начинается с пометки и ссылки на репозитории моста