From e744ef1212336a9e6054ec3a20de8a1c4da8ff66 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 13 Oct 2026 23:17:32 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20bridge-reliability,=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B2=D1=8B=D0=B4=D0=B0=D1=87=D0=B8=20=D0=B8=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20v2=20=D0=B2?= =?UTF-8?q?=20openspec/specs=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- .../.openspec.yaml | 0 .../2026-10-13-bridge-reliability}/design.md | 0 .../proposal.md | 0 .../specs/agent/spec.md | 0 .../specs/commands/spec.md | 0 .../specs/data-model/spec.md | 0 .../specs/hub-api/spec.md | 0 .../specs/mcp/spec.md | 0 .../specs/migration/spec.md | 0 openspec/specs/agent/spec.md | 43 +++++++++++----- openspec/specs/commands/spec.md | 50 ++++++++++++++----- openspec/specs/data-model/spec.md | 24 ++++++--- openspec/specs/hub-api/spec.md | 31 ++++++++++-- openspec/specs/mcp/spec.md | 6 ++- openspec/specs/migration/spec.md | 11 +++- 16 files changed, 127 insertions(+), 40 deletions(-) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/.openspec.yaml (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/design.md (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/proposal.md (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/specs/agent/spec.md (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/specs/commands/spec.md (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/specs/data-model/spec.md (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/specs/hub-api/spec.md (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/specs/mcp/spec.md (100%) rename openspec/changes/{bridge-reliability => archive/2026-10-13-bridge-reliability}/specs/migration/spec.md (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 1406bbc..04d799e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # jira-hub ## Что это -Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; текущий change — `openspec/changes/bridge-reliability/`. +Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; текущего change нет (последний — `openspec/changes/archive/2026-10-13-bridge-reliability/`). ## Стек и ограничения - Python 3.13, uv, hatchling, pytest; HTTP на stdlib (`http.server`), БД — SQLite (stdlib), MCP — пакет `mcp` (закреплён `<2`, рулинг R-10). diff --git a/openspec/changes/bridge-reliability/.openspec.yaml b/openspec/changes/archive/2026-10-13-bridge-reliability/.openspec.yaml similarity index 100% rename from openspec/changes/bridge-reliability/.openspec.yaml rename to openspec/changes/archive/2026-10-13-bridge-reliability/.openspec.yaml diff --git a/openspec/changes/bridge-reliability/design.md b/openspec/changes/archive/2026-10-13-bridge-reliability/design.md similarity index 100% rename from openspec/changes/bridge-reliability/design.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/design.md diff --git a/openspec/changes/bridge-reliability/proposal.md b/openspec/changes/archive/2026-10-13-bridge-reliability/proposal.md similarity index 100% rename from openspec/changes/bridge-reliability/proposal.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/proposal.md diff --git a/openspec/changes/bridge-reliability/specs/agent/spec.md b/openspec/changes/archive/2026-10-13-bridge-reliability/specs/agent/spec.md similarity index 100% rename from openspec/changes/bridge-reliability/specs/agent/spec.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/specs/agent/spec.md diff --git a/openspec/changes/bridge-reliability/specs/commands/spec.md b/openspec/changes/archive/2026-10-13-bridge-reliability/specs/commands/spec.md similarity index 100% rename from openspec/changes/bridge-reliability/specs/commands/spec.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/specs/commands/spec.md diff --git a/openspec/changes/bridge-reliability/specs/data-model/spec.md b/openspec/changes/archive/2026-10-13-bridge-reliability/specs/data-model/spec.md similarity index 100% rename from openspec/changes/bridge-reliability/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/specs/data-model/spec.md diff --git a/openspec/changes/bridge-reliability/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-13-bridge-reliability/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/bridge-reliability/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/specs/hub-api/spec.md diff --git a/openspec/changes/bridge-reliability/specs/mcp/spec.md b/openspec/changes/archive/2026-10-13-bridge-reliability/specs/mcp/spec.md similarity index 100% rename from openspec/changes/bridge-reliability/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/specs/mcp/spec.md diff --git a/openspec/changes/bridge-reliability/specs/migration/spec.md b/openspec/changes/archive/2026-10-13-bridge-reliability/specs/migration/spec.md similarity index 100% rename from openspec/changes/bridge-reliability/specs/migration/spec.md rename to openspec/changes/archive/2026-10-13-bridge-reliability/specs/migration/spec.md diff --git a/openspec/specs/agent/spec.md b/openspec/specs/agent/spec.md index c8ee408..8a12c65 100644 --- a/openspec/specs/agent/spec.md +++ b/openspec/specs/agent/spec.md @@ -25,34 +25,53 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. ### Requirement: Цикл выполнения Агент SHALL работать одним процессом: `claim` к хабу (долгий запрос) → при получении команды: досыпать до `pause_seconds` от последнего вызова Jira → запись «started» в журнал → выполнение по контракту `commands` → запись -«finished» с результатом → отчёт хабу, повторяемый до подтверждения. Одна команда в полёте. При изменении -`settings_version` в ответе `claim` агент SHALL перечитать настройки до выполнения команды. +«finished» с результатом → отчёт хабу, повторяемый до подтверждения. Одна команда в полёте: новый `claim` агент SHALL +отправлять, только когда у него нет выданной, но не завершённой и не отчитанной команды (на этом хаб строит повторную +выдачу, см. `commands`). Один процесс на журнал: `run` SHALL брать исключительную блокировку файла `<журнал>.lock` и, +если она занята, завершаться с кодом 1 и сообщением «агент уже запущен». При изменении `settings_version` в ответе `claim` агент SHALL перечитать настройки до выполнения +команды. #### Scenario: Остановка между командами - **WHEN** получен Ctrl-C во время выполнения команды - **THEN** текущая команда доделывается и отчёт отправляется, затем процесс завершается +#### Scenario: Второй процесс с тем же журналом +- **WHEN** агент работает, и запускается ещё один `agent.py run` с тем же `journal.jsonl` +- **THEN** второй процесс пишет «агент уже запущен…» и завершается с кодом 1, не обращаясь к хабу и Jira + ### 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 дней. +Агент SHALL вести `journal.jsonl` (только дозапись) рядом с собой: строки `claimed {command_id, at}` (сразу после +получения команды, до паузы), `started {command_id, at}`, `finished {command_id, status, result, error, jira_calls, +rate_limited, at}`, `acked {command_id, at}`. При старте агент SHALL: отправить хабу все `finished` без `acked`; для +`claimed`/`started` без `finished` — запросить команду у хаба по id и, если она в хабе всё ещё `claimed`, выполнить её +заново по правилу «проверь, потом сделай» с паузой `pause_seconds` из ответа хаба, не записывая `claimed` повторно. +Команда `python3 agent.py prune` SHALL удалять из журнала записи с `acked` старше 30 дней. #### Scenario: Падение после ответа Jira - **WHEN** агент упал между `POST /issue` и записью `finished`, затем перезапущен - **THEN** агент находит в журнале `started` без `finished`, запрашивает команду у хаба, находит созданную задачу поиском дубля и отчитывается `done` с её ключом; второй задачи нет +#### Scenario: Восстановление без лишней записи +- **WHEN** в журнале `claimed 40` без `finished`, хаб отдаёт команду 40 в `claimed` с `pause_seconds = 30` +- **THEN** в журнал дописываются `started 40`, `finished 40`, `acked 40`; второй строки `claimed 40` нет; перед выполнением + агент выдерживает 30 с от последнего вызова Jira + ### Requirement: Ограничение темпа и 429 -Агент SHALL помнить время последнего вызова Jira (любого) и не начинать команду раньше `pause_seconds` после него. -На HTTP 429 и 503 агент SHALL ждать `Retry-After` (иначе 60 с × номер попытки) и повторять тот же запрос до 5 раз, -считая попытки в `rate_limited`; после 5 неудач — `failed`. Пауза между записями внутри одной команды — по зазору типа команды. +Агент SHALL помнить время последнего вызова Jira (любого) и не начинать команду раньше `pause_seconds` после него. При +старте агент SHALL принять за время последнего вызова Jira самое позднее `at` в журнале (если оно не в будущем); при пустом +журнале паузы перед первой командой нет. На 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` +#### Scenario: Перезапуск не сбивает темп +- **WHEN** последняя запись журнала `finished` сделана 10 с назад, агент перезапущен и получил команду с `pause_seconds = 30` +- **THEN** агент ждёт около 20 с перед первым вызовом Jira по команде + ### Requirement: Чтение состояния `refresh` SHALL выполняться запросом `search` (`jql`, `fields` = поля зеркала включая `issuelinks` и спринт, `maxResults = 100`, постранично) и отправляться хабу пачками по 100 задач со связями; ключи из `issuelinks`, которых нет @@ -66,8 +85,8 @@ status, result, error, jira_calls, rate_limited}`, `acked {command_id}`. При ### Requirement: Доставка и запуск README агента SHALL описывать: копирование `agent.py` и `agent.example.json` через общую папку, заполнение `agent.json` (включая выбор транспорта в `hub_url`), `check`, запуск в терминале и через `nohup` с логом, опциональный launchd-plist, -`prune`. Версия агента SHALL быть в константе файла (2 — с транспортом через папку) и в каждом отчёте; хаб предупреждает -в `agent_status`, если версия ниже минимальной из настроек. +`prune`. Версия агента SHALL быть в константе файла (3 — с восстановлением темпа после перезапуска и блокировкой одного процесса) и в каждом отчёте; +хаб предупреждает в `agent_status`, если версия ниже минимальной из настроек. #### Scenario: Устаревший агент - **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2` diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md index 35e3f13..177a60e 100644 --- a/openspec/specs/commands/spec.md +++ b/openspec/specs/commands/spec.md @@ -56,14 +56,19 @@ SHALL отвергаться при создании пакета (422). ### Requirement: Пакет и порядок выполнения Пакет SHALL создаваться одним вызовом с автором, заголовком и списком команд; хаб SHALL назначить `seq` по порядку списка -и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh`). Очередь SHALL -выдавать команды в порядке `(batch.created_at, seq)`, одну в полёте. Пакет и его `queued` команды MAY быть отменены; -`claimed` команда MUST NOT отменяться автоматически. +и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh`). Пакет с двумя и более +`ensure_issue` с одинаковой `summary` (после обрезки пробелов по краям) SHALL отвергаться (422). Очередь SHALL выдавать +команды в порядке `(batch.created_at, seq)`, одну в полёте. Пакет и его `queued` команды MAY быть отменены; `claimed` +команда MUST NOT отменяться автоматически. #### Scenario: Оценка длительности - **WHEN** создаётся пакет из 3 `ensure_fields` и 2 `ensure_comment` - **THEN** `estimate_seconds` = сумма пауз по политике для последовательности типов + пауза `refresh` +#### Scenario: Две задачи с одной темой +- **WHEN** пакет содержит `ensure_issue {temp_key: new:1, summary: "Отчёт"}` и `ensure_issue {temp_key: new:2, summary: " Отчёт "}` +- **THEN** 422 с темой «Отчёт», пакет не создан + ### Requirement: Политика темпа и пауза на команде Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и `ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint` 30, `refresh` 2). При выдаче команды @@ -76,14 +81,35 @@ SHALL выдерживать зазор типа команды между за - **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 делать; повтор — новая команда с теми же параметрами по запросу интерфейса. +Ошибка команды SHALL записываться в `error` (HTTP-код, текст Jira до 1500 символов), пакет продолжается. + +**Повторная выдача.** Агент запрашивает новую команду, только когда у него нет незавершённой (см. `agent`), поэтому +команда в `claimed` к моменту следующего `claim` считается не доставленной или потерянной агентом. При `claim` хаб SHALL +сначала выдать снова самую старую (по `claimed_at`, затем `id`) команду в `claimed`: тот же `id` и параметры, +`pause_seconds` по текущей политике темпа, новый `claimed_at`, `deliveries + 1`. Если у неё уже `deliveries >= 3`, хаб SHALL +перевести её в `failed` с `error = "агент не отчитался после 3 выдач"` и перейти к следующей (довыдача другой `claimed` +или следующая `queued`). Довыдача — доставка той же выдачи, а не повтор команды: новых команд хаб MUST NOT создавать +сам, повтор после `failed` — новая команда с теми же параметрами по запросу интерфейса. + +**Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб +SHALL перевести её в `confirmed_by_state`: `ensure_fields` — все поля совпадают; `ensure_status` — статус равен последнему +в пути; `ensure_link` — связь есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment` +и `refresh` по состоянию не подтверждаются: потерянный отчёт по ним закрывается повторной выдачей (агент находит +созданную задачу поиском дубля) или отменой человеком. #### Scenario: Отчёт потерян, факт на месте -- **WHEN** `ensure_sprint {key: X, sprint: "Спринт 2"}` в `claimed` 20 минут, агент перезапущен, `refresh` показал X в «Спринт 2» -- **THEN** команда `confirmed_by_state`, следующая команда очереди выдаётся +- **WHEN** `ensure_sprint {key: X, sprint: "Спринт 2"}` в `claimed`, после выполненного `refresh` зеркало показывает X в «Спринт 2» +- **THEN** команда `confirmed_by_state` + +#### Scenario: Ответ на claim потерян +- **WHEN** команда 40 выдана (`deliveries = 1`), ответ до агента не дошёл, агент снова прислал `claim` +- **THEN** `claim` отдаёт команду 40, `deliveries = 2`; в очереди есть `queued` команды, но выдаётся 40 + +#### Scenario: Предел выдач +- **WHEN** команда 41 в `claimed` с `deliveries = 3`, агент снова прислал `claim`, в очереди есть команда 42 +- **THEN** 41 — `failed` «агент не отчитался после 3 выдач»; `claim` отдаёт 42 + +#### Scenario: Потерянный отчёт о создании задачи +- **WHEN** `ensure_issue {temp_key: new:5, summary: "Отчёт"}` выполнена агентом, отчёт не дошёл, агент прислал `claim` +- **THEN** команда выдаётся снова; агент находит созданную задачу поиском дубля и отчитывается `done` с её ключом; второй + задачи нет, заметка `new:5` перенесена на ключ diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md index c879894..32ef019 100644 --- a/openspec/specs/data-model/spec.md +++ b/openspec/specs/data-model/spec.md @@ -8,12 +8,19 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. ### Requirement: Схема базы хаба База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_note`, `batch`, `command`, `sync_run`, `agent_status`, `setting`. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру версии -в `setting`. Денежных и дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени). +в `setting.schema_version` (текущая — 2). Миграция существующей базы SHALL выполняться в одной транзакции; перед ней +хаб SHALL скопировать файл базы в `backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой. Денежных и дробных +полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени). #### Scenario: Первый запуск - **WHEN** хаб стартует с несуществующим файлом базы - **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая +#### Scenario: Миграция базы v1 +- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed` +- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); данные на месте; + `schema_version = 2` + ### 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-список), @@ -35,7 +42,7 @@ SHALL хранить связи `(source_key, target_key, link_type)` с нап Таблица `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 переименовать строку заметок -на настоящий ключ. +на настоящий ключ — в том числе по позднему отчёту `done` с ключом о команде, отменённой до отчёта. #### Scenario: Заметка переживает refresh - **WHEN** у задачи есть `issue_note.fact_status = "В работе"` и приходит полное обновление зеркала @@ -44,9 +51,10 @@ MUST NOT менять `issue_note`. При завершении `ensure_issue` ### 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 вычисляться из команд, не храниться. +(`queued | claimed | done | failed | skipped | cancelled | confirmed_by_state`), `claimed_at` (время последней выдачи), +`deliveries` (сколько раз команда выдана агенту), `finished_at`, `result` (JSON), `error`, `jira_calls`, `rate_limited` +(число 429/503), `pause_seconds` (выданная пауза), `depends_on` (id команды, чей результат подставляется). Статус пакета +SHALL вычисляться из команд, не храниться. #### Scenario: Статус пакета - **WHEN** в пакете 3 команды: `done`, `claimed`, `queued` @@ -55,11 +63,13 @@ MUST NOT менять `issue_note`. При завершении `ensure_issue` ### 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`. +хранить `last_seen_at`, `agent_version`, `settings_version`, `current_command_id`, `last_jira_call_at`. Через API и MCP +`agent_status` SHALL отдаваться с признаком `online` (истина, если `last_seen_at` моложе 5 минут) и списком +`redelivered` — команд в `claimed`, выданных больше одного раза (`id`, `type`, `claimed_at`, `deliveries`). #### Scenario: Агент давно не выходил на связь - **WHEN** `last_seen_at` старше 5 минут -- **THEN** `agent_status` через API и MCP отдаётся с признаком `offline` +- **THEN** `agent_status` через API и MCP отдаётся с `online = false` ### Requirement: Настройки Таблица `setting` SHALL хранить парами ключ–JSON: `base_jql`, `field_ids`, `issue_types`, `priority_map`, `link_types`, diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index 29fd610..daeeeba 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -19,10 +19,14 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. Хаб 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`. + Если есть команда в `claimed`, `claim` SHALL выдать её снова (повторная выдача, см. `commands`), а не следующую из + очереди. - `GET /agent/settings` — настройки для агента (без токенов). -- `GET /agent/commands/{id}` — команда по id (для восстановления из журнала). +- `GET /agent/commands/{id}` — команда по id (для восстановления из журнала), включая `pause_seconds`. - `POST /agent/commands/{id}/result` `{status, result, error, jira_calls, rate_limited}` — идемпотентно: повторный - отчёт по команде в терминальном статусе отвечает 200 без изменений. + отчёт по команде в терминальном статусе отвечает 200 без изменений, кроме позднего отчёта `done` с `result.key` по + отменённой `ensure_issue` без результата (результат записывается, статус остаётся `cancelled`, заметка `new:N` + переносится на ключ). Отчёт по команде в `queued` SHALL отвергаться 409 `not_claimed`. - `POST /agent/state` `{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}` — загрузка состояния пачками; `neighbor_keys` — ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало и `sync_run`. - `GET /agent/known-keys` — ключи зеркала (для определения соседей). @@ -35,6 +39,10 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. - **WHEN** агент дважды прислал `result` для команды 15 - **THEN** второй ответ 200, статус и результат не изменились +#### Scenario: Отчёт по невыданной команде +- **WHEN** `POST /agent/commands/16/result` с `status = done`, команда 16 в `queued` +- **THEN** 409 `not_claimed`, команда осталась `queued` + ### Requirement: Маршруты интерфейсов Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном: - `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, @@ -43,7 +51,8 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. - `GET /issues/{key}` — задача целиком: поля, связи в обе стороны, заметки, последние 10 команд по ней. - `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела). - `POST /batches` `{title, author, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, неверных - параметрах, временном ключе без `ensure_issue`. + параметрах, временном ключе без `ensure_issue`, двух и более `ensure_issue` с одинаковой `summary` (сравнение после + обрезки пробелов по краям; в ответе перечислены повторяющиеся темы). - `GET /batches?limit=`, `GET /batches/{id}` (с командами), `POST /batches/{id}/cancel`. - `POST /commands/{id}/retry` — новая команда с теми же параметрами в новом пакете «Повтор команды N». - `POST /commands/{id}/cancel` — только для `queued`; для `claimed` — 409, если не передан `force: true`. @@ -56,7 +65,15 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. #### Scenario: Отмена взятой команды - **WHEN** `POST /commands/15/cancel` без `force`, команда `claimed` -- **THEN** 409 с `claimed_at`; с `force: true` — `cancelled`, следующий отчёт агента по ней принимается, но статус не меняется +- **THEN** 409 с `claimed_at`; с `force: true` — `cancelled`, следующий отчёт агента по ней принимается, статус не меняется + +#### Scenario: Поздний отчёт по отменённому созданию +- **WHEN** `ensure_issue {temp_key: new:7, …}` отменена с `force: true`, затем агент прислал `done` с `result.key = AURORA-1620` +- **THEN** команда `cancelled`, в `result` — `AURORA-1620`; заметка `new:7` перенесена на `AURORA-1620` + +#### Scenario: Дубль темы в пакете +- **WHEN** `POST /batches` с двумя `ensure_issue`, у обеих `summary = "Шахматка: комментарии"` +- **THEN** 422 с темой «Шахматка: комментарии», пакет не создан ### Requirement: Резервные копии Перед выдачей первой команды пакета, содержащего записи в Jira, хаб SHALL скопировать файл базы в `backups/<дата-время>-.sqlite` @@ -68,12 +85,16 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. ### Requirement: Автообновление зеркала Если `auto_refresh_minutes > 0`, агент на связи и очередь пуста дольше этого интервала, хаб SHALL поставить пакет -«Автообновление» из `refresh {jql: base_jql}`. +«Автообновление» из `refresh {jql: base_jql}`. Очередь не пуста, если есть команды в `queued` или `claimed`. #### Scenario: Очередь занята - **WHEN** интервал прошёл, но в очереди есть `queued` команды - **THEN** автообновление не ставится до опустошения очереди +#### Scenario: Есть выданная команда без отчёта +- **WHEN** интервал прошёл, `queued` нет, одна команда в `claimed` +- **THEN** автообновление не ставится; команда довыдаётся при следующем `claim` + ### Requirement: Реле через общую папку `jira-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md index 1caaea3..bea0da5 100644 --- a/openspec/specs/mcp/spec.md +++ b/openspec/specs/mcp/spec.md @@ -26,7 +26,7 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. | `batches` | `limit` | последние пакеты со статусом | | `command_retry`, `batch_cancel` | `id` | новый пакет / результат отмены | | `refresh` | `keys` или `full`, `wait` (bool) | `sync_run` или id пакета | -| `agent_status` | — | онлайн/офлайн, версия, текущая команда, 429 за сутки, версия настроек | +| `agent_status` | — | онлайн/офлайн, версия, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек | | `sync_log` | `limit` | последние `sync_run` | | `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` | @@ -39,3 +39,7 @@ TBD - created by archiving change bridge-core. Update Purpose after archive. #### Scenario: Ожидание результата - **WHEN** `batch_wait {id, timeout_minutes: 10}` и пакет завершился через 6 минут - **THEN** ответ через 6 минут со статусами всех команд + +#### Scenario: Статус агента с довыдачей +- **WHEN** агент на связи, команда 40 выдана дважды и ещё без отчёта +- **THEN** ответ `agent_status` содержит «агент онлайн» и строку о команде 40 с числом выдач 2 diff --git a/openspec/specs/migration/spec.md b/openspec/specs/migration/spec.md index a19999f..176c3cc 100644 --- a/openspec/specs/migration/spec.md +++ b/openspec/specs/migration/spec.md @@ -20,12 +20,19 @@ MUST NOT переноситься (они у агента). `jira-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.csv` и заполнить `issue_note` (ключ Jira; для строк без ключа — `new:<номер строки>`): статус по факту, вид работы, что сделано, что осталось, внутренние колонки. `links.csv` SHALL импортироваться как сверочный набор в отчёт импорта (ожидаемые связи -против зеркала после первого `refresh`), не как команды. Импорт идемпотентен: повтор обновляет заметки, не дублирует. +против зеркала после первого `refresh`), не как команды. Связь, которой в зеркале нет как `source→target` того же типа, +но есть как `target→source` того же типа, SHALL попадать в `reversed_links`, а не в `missing_links`. Импорт идемпотентен: +повтор обновляет заметки, не дублирует. #### Scenario: Импорт и первая синхронизация - **WHEN** выполнены `init`, `import`, запущены хаб и агент, `refresh full` завершён - **THEN** число задач зеркала ≥ числа строк `tasks.csv` + созданных из `new_tasks.csv`; отчёт импорта перечисляет ключи - из CSV, которых нет в зеркале, и связи из `links.csv`, которых нет в `issue_link` + из CSV, которых нет в зеркале, связи из `links.csv`, которых нет в `issue_link`, и связи, которые есть в обратную сторону + +#### Scenario: Связь в обратную сторону +- **WHEN** в `links.csv` строка «AURORA-1445 → AURORA-1446, связанные», а в зеркале есть только «ASUPR_Связанные» + 1446→1445 +- **THEN** связь в `reversed_links`, в `missing_links` её нет ### Requirement: Что остаётся в admin/ После ввода моста `admin/jira-sync/README.md` SHALL получить в начале пометку «заменено мостом jira-hub/jira-agent,