diff --git a/openspec/changes/bridge-core/.openspec.yaml b/openspec/changes/archive/2026-10-12-bridge-core/.openspec.yaml similarity index 100% rename from openspec/changes/bridge-core/.openspec.yaml rename to openspec/changes/archive/2026-10-12-bridge-core/.openspec.yaml diff --git a/openspec/changes/bridge-core/design.md b/openspec/changes/archive/2026-10-12-bridge-core/design.md similarity index 100% rename from openspec/changes/bridge-core/design.md rename to openspec/changes/archive/2026-10-12-bridge-core/design.md diff --git a/openspec/changes/bridge-core/proposal.md b/openspec/changes/archive/2026-10-12-bridge-core/proposal.md similarity index 100% rename from openspec/changes/bridge-core/proposal.md rename to openspec/changes/archive/2026-10-12-bridge-core/proposal.md diff --git a/openspec/changes/bridge-core/specs/agent/spec.md b/openspec/changes/archive/2026-10-12-bridge-core/specs/agent/spec.md similarity index 100% rename from openspec/changes/bridge-core/specs/agent/spec.md rename to openspec/changes/archive/2026-10-12-bridge-core/specs/agent/spec.md diff --git a/openspec/changes/bridge-core/specs/commands/spec.md b/openspec/changes/archive/2026-10-12-bridge-core/specs/commands/spec.md similarity index 100% rename from openspec/changes/bridge-core/specs/commands/spec.md rename to openspec/changes/archive/2026-10-12-bridge-core/specs/commands/spec.md diff --git a/openspec/changes/bridge-core/specs/data-model/spec.md b/openspec/changes/archive/2026-10-12-bridge-core/specs/data-model/spec.md similarity index 100% rename from openspec/changes/bridge-core/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-12-bridge-core/specs/data-model/spec.md diff --git a/openspec/changes/bridge-core/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-12-bridge-core/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/bridge-core/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-12-bridge-core/specs/hub-api/spec.md diff --git a/openspec/changes/bridge-core/specs/mcp/spec.md b/openspec/changes/archive/2026-10-12-bridge-core/specs/mcp/spec.md similarity index 100% rename from openspec/changes/bridge-core/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-12-bridge-core/specs/mcp/spec.md diff --git a/openspec/changes/bridge-core/specs/migration/spec.md b/openspec/changes/archive/2026-10-12-bridge-core/specs/migration/spec.md similarity index 100% rename from openspec/changes/bridge-core/specs/migration/spec.md rename to openspec/changes/archive/2026-10-12-bridge-core/specs/migration/spec.md diff --git a/openspec/specs/agent/spec.md b/openspec/specs/agent/spec.md new file mode 100644 index 0000000..2027367 --- /dev/null +++ b/openspec/specs/agent/spec.md @@ -0,0 +1,67 @@ +# agent Specification + +## Purpose +TBD - created by archiving change bridge-core. Update Purpose after archive. + +## 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` = «обновите агента», команды выдаются по-прежнему diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md new file mode 100644 index 0000000..35e3f13 --- /dev/null +++ b/openspec/specs/commands/spec.md @@ -0,0 +1,89 @@ +# commands Specification + +## Purpose +TBD - created by archiving change bridge-core. Update Purpose after archive. + +## Requirements + +### Requirement: Команда — желаемый результат на одной задаче +Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить → +записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done` +без записи. Типы команд и их контракт: + +| Тип | Параметры | Что читает | Что пишет | +|---|---|---|---| +| `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = 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:`. Хаб 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 показываться как «нет ответа от агента с ». Если после любого `refresh` желаемый +результат такой команды виден в зеркале (для `ensure_issue` — задача с той же темой от нас; `ensure_fields` — все поля +совпадают; `ensure_status` — статус равен последнему в пути; `ensure_link` — связь есть и конфликтов нет; `ensure_sprint` — +спринт совпадает), хаб SHALL перевести её в `confirmed_by_state`. Для `ensure_comment` и `refresh` подтверждения по +состоянию нет: комментарии в зеркале не хранятся, такая команда ждёт отчёта агента или отмены человеком. Автоматических повторов хаб +MUST NOT делать; повтор — новая команда с теми же параметрами по запросу интерфейса. + +#### Scenario: Отчёт потерян, факт на месте +- **WHEN** `ensure_sprint {key: X, sprint: "Спринт 2"}` в `claimed` 20 минут, агент перезапущен, `refresh` показал X в «Спринт 2» +- **THEN** команда `confirmed_by_state`, следующая команда очереди выдаётся diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md new file mode 100644 index 0000000..2a85a06 --- /dev/null +++ b/openspec/specs/data-model/spec.md @@ -0,0 +1,72 @@ +# data-model Specification + +## Purpose +TBD - created by archiving change bridge-core. Update Purpose after archive. + +## 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:`), +`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` отдаёт агенту новую версию diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md new file mode 100644 index 0000000..0a0a137 --- /dev/null +++ b/openspec/specs/hub-api/spec.md @@ -0,0 +1,75 @@ +# hub-api Specification + +## Purpose +TBD - created by archiving change bridge-core. Update Purpose after archive. + +## Requirements + +### Requirement: Процесс и доступ +`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:` (по умолчанию 8790), база и настройки в +`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты SHALL требовать `Authorization: Bearer `: +агентский токен открывает только `/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: [...], neighbor_keys: [...], page, last_page}` — загрузка состояния + пачками; `neighbor_keys` — ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало и `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/<дата-время>-.sqlite` +и хранить последние `backup_keep` копий. + +#### Scenario: Ротация +- **WHEN** копий больше `backup_keep` +- **THEN** самые старые удаляются до `backup_keep` + +### Requirement: Автообновление зеркала +Если `auto_refresh_minutes > 0`, агент на связи и очередь пуста дольше этого интервала, хаб SHALL поставить пакет +«Автообновление» из `refresh {jql: base_jql}`. + +#### Scenario: Очередь занята +- **WHEN** интервал прошёл, но в очереди есть `queued` команды +- **THEN** автообновление не ставится до опустошения очереди diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md new file mode 100644 index 0000000..1caaea3 --- /dev/null +++ b/openspec/specs/mcp/spec.md @@ -0,0 +1,41 @@ +# mcp Specification + +## Purpose +TBD - created by archiving change bridge-core. Update Purpose after archive. + +## 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 минут со статусами всех команд diff --git a/openspec/specs/migration/spec.md b/openspec/specs/migration/spec.md new file mode 100644 index 0000000..a19999f --- /dev/null +++ b/openspec/specs/migration/spec.md @@ -0,0 +1,37 @@ +# migration Specification + +## Purpose +TBD - created by archiving change bridge-core. Update Purpose after archive. + +## Requirements + +### Requirement: Инициализация настроек +`jira-hub init --config ` 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 пакета начинается с пометки и ссылки на репозитории моста