docs(openspec): bridge-reliability упрощена — R-11 закрывается довыдачей, O-3 через reversed_links (#9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8b321793ca
commit
84cce848e7
@ -9,102 +9,106 @@
|
||||
`_next` обходит пакет, чей определитель `new:N` висит в `claimed` дольше 10 минут (R-13).
|
||||
- `report_result` для терминального статуса возвращает команду без изменений (результат отбрасывается); статус `queued`
|
||||
не проверяется.
|
||||
- `check_confirmed(…, reporter)` для `ensure_issue` при `reporter is None` не подтверждает; `reporter` берётся из настройки,
|
||||
которая никогда не задаётся. Для `ensure_link` входящая связь любого типа из `link_types` — конфликт.
|
||||
- `reconcile` после отчёта `refresh` подтверждает `claimed` команды по зеркалу (`check_confirmed`); для `ensure_issue`
|
||||
нужен `reporter` из настройки, которая никогда не задаётся, — не подтверждается никогда.
|
||||
- Отчёт импорта: связь из `links.csv` «есть», только если в зеркале `source→target` того же типа.
|
||||
- Агент: `JiraClient.last_call_at` (monotonic) только в памяти; `Runner.wait_pause` без `last_call_at` не ждёт;
|
||||
`recover` ставит `cmd["pause_seconds"] = 0` и вызывает `execute`, который пишет `claimed` ещё раз.
|
||||
Агент приходит за новой командой (`claim`) только когда своей незавершённой нет: каждый путь `execute` кончается
|
||||
подтверждённым отчётом или остановкой процесса, `recover` выполняется до первого `claim`.
|
||||
- `symmetric_types()` агента уже определяет симметричные типы связей по `GET /issueLinkType` (`inward == outward`).
|
||||
`recover` ставит `cmd["pause_seconds"] = 0` и вызывает `execute`, который пишет `claimed` ещё раз. Агент приходит за
|
||||
новой командой (`claim`) только когда своей незавершённой нет: каждый путь `execute` кончается подтверждённым отчётом
|
||||
или остановкой процесса, `recover` выполняется до первого `claim`. Единственное место, где агент сам теряет выданную
|
||||
команду, — `reload_settings_before` при недоступном хабе без остановки (команда не пишется в журнал, цикл идёт на
|
||||
новый `claim`); довыдача закрывает и его.
|
||||
- `db.migrate` только создаёт таблицы (`CREATE TABLE IF NOT EXISTS`) и пишет `schema_version = 1`; миграций по версии нет.
|
||||
`db.connect` вызывается на каждый запрос API и при старте.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
Goals: ни одна команда не застревает в `claimed` без пути восстановления, кроме явного предела выдач; `ensure_issue` и
|
||||
`ensure_link` подтверждаются по состоянию; созданная задача не теряется при отмене; после перезапуска агента темп Jira
|
||||
соблюдается; живая база мигрирует без потери данных; агент v2 продолжает работать с новым хабом.
|
||||
Goals: ни одна команда не застревает в `claimed` без пути восстановления, кроме явного предела выдач; созданная задача
|
||||
не теряется при отмене; после перезапуска агента темп Jira соблюдается; живая база мигрирует без потери данных; агент v2
|
||||
продолжает работать с новым хабом; отчёт импорта не даёт ложных `missing_links` для связей в обратную сторону.
|
||||
Non-goals — см. proposal.md.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Повторная выдача (решение владельца, вариант A).** Если при `claim` есть команды в `claimed`, хаб выдаёт самую
|
||||
старую из них (по `claimed_at`, затем `id`) снова: тот же `id`, те же параметры, `pause_seconds` по текущей политике
|
||||
темпа, новый `claimed_at`, `deliveries + 1`. Основание — инвариант агента «новый `claim` только без незавершённой
|
||||
команды»: раз агент пришёл, выданную он не получил или потерял. Все команды идемпотентны («проверь, потом сделай»),
|
||||
поэтому довыдача уже выполненной команды даёт `done` без записи. Это не повтор в смысле правила «хаб не повторяет
|
||||
команды сам»: новая команда не создаётся, решение о повторе после `failed` по-прежнему за человеком.
|
||||
2. **Предел выдач — 3.** Если у команды в `claimed` уже `deliveries >= 3` и агент снова пришёл за командой, хаб переводит
|
||||
её в `failed` с `error = "агент не отчитался после 3 выдач"`, `finished_at = now`, и выбирает следующую по обычным
|
||||
правилам (довыдача другой `claimed` или `_next`). Защищает очередь от команды, на которой агент падает. Число 3 —
|
||||
константа кода, не настройка.
|
||||
3. **Удаляются `_fresh_claimed_exists` и R-13.** При довыдаче `claimed` команда не может «зависнуть» без агента: если
|
||||
агента нет, очередь всё равно стоит. Обход пакета с зависшим определителем (`_definer_stale`, `blocked_batches`) больше
|
||||
не нужен — определитель довыдаётся. `maybe_auto_refresh` вместо «свежей claimed» проверяет «есть любая claimed».
|
||||
`agent_view.stuck` заменяется на `redelivered` — команды в `claimed` с `deliveries > 1`.
|
||||
4. **Миграция схемы v1 → v2.** `SCHEMA_VERSION = 2`; `migrate` по номеру из `setting.schema_version`: для v1 — `ALTER TABLE
|
||||
command ADD COLUMN deliveries INTEGER NOT NULL DEFAULT 0`, `ALTER TABLE agent_status ADD COLUMN jira_user TEXT`,
|
||||
`ALTER TABLE agent_status ADD COLUMN symmetric_link_types TEXT` (JSON-список), затем `schema_version = 2`, всё в одной
|
||||
транзакции. Существующие `claimed` команды получают `deliveries = 1` (их уже выдали один раз). Новая база создаётся сразу
|
||||
в v2. Перед миграцией существующей базы хаб копирует файл в `backups/<дата-время>-schema-v1.sqlite`.
|
||||
5. **Отчёты.** `report_result` по команде в `queued` — `ValueError`-подобная ошибка, API отвечает 409 «команда не выдана».
|
||||
Агент на 409 уже считает команду закрытой (`send_result`), правка агента не нужна. По команде в `cancelled` типа
|
||||
`ensure_issue` с `status = done` и `result.key`: результат и `finished_at` записываются, статус остаётся `cancelled`,
|
||||
заметка `new:N` переименовывается на ключ. Для остальных терминальных статусов — без изменений, как сейчас.
|
||||
6. **Дубль темы в пакете — 422.** `validate` пакета отвергает две и более `ensure_issue` с одинаковой `summary` (после
|
||||
обрезки пробелов по краям), перечисляя темы. Регистр не нормализуется: поиск дубля агентом — точное сравнение.
|
||||
7. **Факты агента о Jira — в claim.** Тело `claim` получает необязательные `jira_user` (строка, `name` из `GET /myself` — то же
|
||||
поле, что агент кладёт в `reporter` зеркала) и `symmetric_link_types` (список имён типов Jira с `inward == outward`).
|
||||
Хаб пишет непустые значения в `agent_status` (`COALESCE`, как остальные поля claim). Это сведения о среде агента, не
|
||||
настройки: требование data-model «логин Jira в настройках MUST NOT» не затрагивается. Заготовка настройки `reporter`
|
||||
удаляется из кода.
|
||||
8. **Подтверждение по состоянию.** `ensure_issue` — задача с той же `summary`, `reporter = agent_status.jira_user`, созданная
|
||||
не раньше 7 дней назад; при `jira_user IS NULL` — не подтверждается (как сейчас). `ensure_link` — связь есть, если в
|
||||
зеркале есть `source→target` нужного типа **или** тип симметричен и есть `target→source` этого типа; конфликт —
|
||||
связь другого типа из `link_types` в любом направлении или обратная связь несимметричного типа. Отчёт импорта
|
||||
(`missing_links`) использует то же правило «есть» (общая функция в `commands`).
|
||||
9. **Агент: сбор фактов.** После первой загрузки настроек и до первого `claim` агент вызывает `GET /myself` и
|
||||
`GET /issueLinkType` (вызовы Jira, обновляют `last_call_at`; пауза следующей команды отсчитывается от них). Ошибка любого
|
||||
из вызовов не останавливает агента: поле не передаётся, агент повторяет сбор перед следующим `claim` не чаще раза в
|
||||
10 минут, пока не получит значение. `symmetric_types()` переиспользует тот же кэш.
|
||||
10. **Агент: темп после перезапуска.** При старте `Runner` берёт максимальное `at` среди записей журнала (любого вида) и,
|
||||
если оно не в будущем, ставит `last_call_at = monotonic() − (now − at)`. Запись `finished` пишется сразу после
|
||||
последнего вызова Jira команды, поэтому оценка не раньше настоящего вызова — ждём не меньше нужного. Пустой журнал —
|
||||
`last_call_at` пуст, как сейчас. `recover` не обнуляет `pause_seconds`: берёт значение из ответа `GET /agent/commands/{id}`.
|
||||
11. **Агент: `claimed` при восстановлении не пишется повторно.** `execute(cmd, journal_claimed=True)`; `recover` вызывает с
|
||||
старую из них (по `claimed_at`, затем `id`) снова: тот же `id`, те же (уже подставленные) параметры, `pause_seconds` по
|
||||
текущей политике темпа, новый `claimed_at`, `deliveries + 1`, `agent_status.current_command_id` = её `id`. Основание —
|
||||
инвариант агента «новый `claim` только без незавершённой команды»: раз агент пришёл, выданную он не получил или
|
||||
потерял. Все команды идемпотентны («проверь, потом сделай»), поэтому довыдача уже выполненной команды даёт `done` без
|
||||
записи; для `ensure_issue` агент находит свою задачу поиском дубля (`reporter = currentUser()`, тема, 7 дней). Это не
|
||||
повтор в смысле правила «хаб не повторяет команды сам»: новая команда не создаётся, решение о повторе после `failed`
|
||||
по-прежнему за человеком. Резервная копия перед первой командой пакета при довыдаче не делается (уже сделана).
|
||||
2. **Предел выдач — 3.** Если у самой старой `claimed` уже `deliveries >= 3`, хаб переводит её в `failed` с
|
||||
`error = "агент не отчитался после 3 выдач"`, `finished_at = now`, снимает `current_command_id` и выбирает дальше по тем
|
||||
же правилам (следующая `claimed` или `_next`). Защищает очередь от команды, на которой агент падает. Число 3 —
|
||||
константа `MAX_DELIVERIES`, не настройка.
|
||||
3. **Удаляются `_fresh_claimed_exists`, `_definer_stale`, `STALE_MINUTES` и обход R-13.** При довыдаче команда не может
|
||||
«зависнуть» без агента: если агента нет, очередь всё равно стоит. `maybe_auto_refresh` вместо «свежей claimed»
|
||||
проверяет «есть любая claimed». `agent_view.stuck` заменяется на `redelivered` — команды в `claimed` с
|
||||
`deliveries > 1` (`id`, `type`, `claimed_at`, `deliveries`).
|
||||
4. **R-11 закрывается довыдачей.** Подтверждение по состоянию остаётся (`reconcile` после `refresh`) для команд, которые
|
||||
окажутся в `claimed` одновременно с выполненным `refresh` (две `claimed` после миграции живой базы, гонка двух `claim`),
|
||||
но для `ensure_issue` оно не делается: `check_confirmed` для неё всегда «нет», параметр `reporter` и чтение настройки
|
||||
`reporter` удаляются. Совпадение темы с задачей неизвестного автора — не доказательство.
|
||||
5. **Миграция схемы v1 → v2.** `SCHEMA_VERSION = 2`; новая база создаётся сразу в v2 (`deliveries` в `CREATE TABLE`).
|
||||
Для существующей базы `migrate` читает `setting.schema_version`; если 1 — копия файла базы через
|
||||
`backup.make_backup(conn, <каталог базы>/backups, "schema-v1", keep=0)` (без ротации), затем в `transaction`: повторная
|
||||
проверка версии (параллельный `connect` мог успеть), `ALTER TABLE command ADD COLUMN deliveries INTEGER NOT NULL
|
||||
DEFAULT 0`, `UPDATE command SET deliveries = 1 WHERE status = 'claimed'`, `schema_version = 2`. Для `:memory:` копия не
|
||||
делается. `<каталог базы>/backups` совпадает с `paths.backups_dir()` при штатном `JIRA_HUB_HOME`.
|
||||
6. **Отчёты.** `report_result` по команде в `queued` — `NotClaimedError` (подкласс `ValueError` не годится: API уже
|
||||
отдаёт 422 на `ValueError`), API отвечает 409 `not_claimed`. Агент на 409 уже считает команду закрытой
|
||||
(`send_result`), правка агента не нужна. По команде в `cancelled` типа `ensure_issue` с `status = done` и
|
||||
`result.key`, если `result` у неё ещё пуст: `result`, `jira_calls`, `rate_limited` записываются, `finished_at` и статус
|
||||
не меняются, заметка `new:N` переименовывается на ключ. Для остальных терминальных статусов — без изменений, как сейчас.
|
||||
7. **Дубль темы в пакете — 422.** `validate` пакета отвергает две и более `ensure_issue` с одинаковой `summary` после
|
||||
`strip()`, перечисляя повторяющиеся темы. Регистр не нормализуется: поиск дубля агентом — точное сравнение.
|
||||
8. **Отчёт импорта: `reversed_links`.** Связь из `links.csv`, которой нет как `source→target` того же типа, но есть
|
||||
`target→source` того же типа, попадает в `reversed_links`, а не в `missing_links`. Симметричность типа хабу не нужна:
|
||||
человек видит, что связь в Jira есть, и решает сам. CLI выводит новое поле в том же JSON.
|
||||
9. **Агент: темп после перезапуска.** При старте `Runner` берёт максимальное `at` среди записей журнала (любого вида) и,
|
||||
если оно не в будущем, ставит `JiraClient.last_call_at = monotonic() − (now − at)`, `last_call_iso = at`. Запись
|
||||
`finished` пишется сразу после последнего вызова Jira команды, поэтому оценка не раньше настоящего вызова — ждём не
|
||||
меньше нужного. Пустой журнал или нечитаемое `at` — `last_call_at` пуст, как сейчас. `recover` не обнуляет
|
||||
`pause_seconds`: берёт значение из ответа `GET /agent/commands/{id}`.
|
||||
10. **Агент: `claimed` при восстановлении не пишется повторно.** `execute(cmd, journal_claimed=True)`; `recover` вызывает с
|
||||
`False` — запись `claimed`/`started` для этого id уже есть.
|
||||
12. **Версия агента 3**, `min_agent_version` не поднимается. Хаб принимает claim без новых полей (агент v2).
|
||||
13. **Признак `online`.** Спека data-model меняется под код: `agent_status` отдаёт `online: false`, если `last_seen_at`
|
||||
11. **Версия агента 3**, `min_agent_version` не поднимается. Тела запросов агента не меняются.
|
||||
12. **Признак `online`.** Спека data-model меняется под код: `agent_status` отдаёт `online: false`, если `last_seen_at`
|
||||
старше 5 минут (решение без обсуждения, см. ниже).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Нарушение инварианта агента (например, будущая параллельность) превратит довыдачу в двойное выполнение — безопасно
|
||||
из-за идемпотентности, но тратит вызовы Jira. Инвариант фиксируется в спеке agent.
|
||||
- Довыдача после долгого простоя агента выполнит старую команду: параметры не устаревают сами — человек мог за это время
|
||||
поменять задачу вручную, команда «вернёт» желаемое значение. Так же вела бы себя команда в очереди; для отмены есть
|
||||
`cancel force`.
|
||||
- Довыдача после долгого простоя агента выполнит старую команду: если человек за это время поменял задачу вручную,
|
||||
команда «вернёт» желаемое значение. Так же вела бы себя команда в очереди; для отмены есть `cancel force`.
|
||||
- Гонка двух `claim` через папку (старый долгий запрос ещё ждёт в хабе, агент прислал новый): старый берёт команду, ответ
|
||||
никто не читает, новый её довыдаёт — `deliveries = 2` без реального сбоя. Предел 3 оставляет запас на один такой
|
||||
случай плюс одну настоящую недоставку.
|
||||
- Миграция живой базы — первая в проекте; страхуется копией перед миграцией и тестом на базе v1.
|
||||
- Оценка паузы из журнала по `at` старше настоящего последнего вызова, если агент упал во время `refresh` между страницами
|
||||
(последний вызов Jira позже последней записи). Пауза `refresh` — 2 с, ошибка мала.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Хаб: merge → перезапуск launchd (`launchctl kickstart -k`) → в логе миграция v1→v2 и копия базы; агент v2 продолжает
|
||||
работу.
|
||||
2. Агент v3: файл через общую папку на виртуалку, `check`, перезапуск `run`; `agent_status` показывает логин.
|
||||
3. Проверка: повторный `jira-hub import admin/jira-sync` — `missing_links: 0`.
|
||||
Откат: остановить хаб, вернуть копию `backups/*-schema-v1.sqlite`, предыдущий код хаба; агент v3 работает с хабом v1
|
||||
(лишние поля claim игнорируются — проверить тестом API).
|
||||
1. Хаб: merge → `git pull` в основном checkout → перезапуск launchd (`launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`)
|
||||
→ в `backups/` копия `*-schema-v1.sqlite`, `schema_version = 2`; агент v2 продолжает работу.
|
||||
2. Агент v3: файл через общую папку на виртуалку, `check`, перезапуск `run`.
|
||||
3. Проверка: повторный `jira-hub import admin/jira-sync` — три связи в `reversed_links`, `missing_links: 0`.
|
||||
Откат: остановить хаб, вернуть копию `backups/*-schema-v1.sqlite` на место `hub.sqlite`, предыдущий код хаба; агент v3
|
||||
работает с хабом v1 (тела запросов не менялись).
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|
|
||||
| 2026-10-13 | Спека data-model приводится к коду: признак `online` (bool), а не `offline` | порог 5 мин совпадает, `online` уже читают MCP и автообновление; правка кода дала бы лишнюю смену контракта | нет: клиентов с `offline` нет |
|
||||
| 2026-10-13 | Предел довыдач — 3, константа кода | одной недоставки на сбой папки хватает с запасом; «ядовитая» команда не держит очередь | если сбоев папки больше 2 подряд на одной команде — ложный `failed`, лечится `command_retry` |
|
||||
| 2026-10-13 | Предел довыдач — 3, константа кода | одной недоставки на сбой папки хватает с запасом; «ядовитая» команда не держит очередь | при 3 сбоях подряд на одной команде — ложный `failed`, лечится `command_retry` |
|
||||
| 2026-10-13 | Миграция v1→v2 с копией базы перед ней; существующим `claimed` — `deliveries = 1` | первая миграция живой базы; честный счёт выдач | нет |
|
||||
| 2026-10-13 | Дубль темы в пакете — после `strip`, без нормализации регистра | агент ищет дубль точным сравнением `summary` | темы, различающиеся только регистром, пройдут — так же их различит агент |
|
||||
| 2026-10-13 | Повторный сбор фактов агента не чаще раза в 10 минут | Jira банка — лимиты; факты меняются редко | логин придёт до 10 минут позже после восстановления Jira |
|
||||
| 2026-10-13 | Оценка последнего вызова Jira после перезапуска — максимальное `at` журнала | `finished` пишется сразу после последнего вызова; без новой записи на каждый вызов Jira | см. Risks: `refresh` между страницами, ошибка ≤ 2 с |
|
||||
| 2026-10-13 | Поздний отчёт по отменённой `ensure_issue`: результат пишется, статус `cancelled` сохраняется | зависимые команды уже `skipped`, смена статуса задним числом запутала бы пакет; ключ виден в результате и в заметке | нет |
|
||||
| 2026-10-13 | `report_result` по `queued` — 409 «команда не выдана» | агент уже трактует 409 как «закрыто», правка агента не нужна | нет |
|
||||
| 2026-10-13 | Поздний отчёт по отменённой `ensure_issue`: результат пишется, статус `cancelled` и `finished_at` сохраняются | зависимые команды уже `skipped`, смена статуса задним числом запутала бы пакет; ключ виден в результате и в заметке | нет |
|
||||
| 2026-10-13 | `report_result` по `queued` — 409 `not_claimed` | агент уже трактует 409 как «закрыто», правка агента не нужна | нет |
|
||||
| 2026-10-13 | Связи «в обратную сторону» в отчёте импорта — отдельный список `reversed_links` без учёта симметричности типа | хаб не знает симметричность; человек видит, что связь есть | несимметричная связь, поставленная наоборот, не попадёт в `missing_links` — видна в `reversed_links` |
|
||||
| 2026-10-13 | Довыдача не делает резервную копию базы | копия перед первой командой пакета уже сделана при первой выдаче | нет |
|
||||
|
||||
@ -4,24 +4,28 @@
|
||||
Отложенные находки `bridge-core` (#1) и `folder-transport` (#5) содержат несколько мест, где живая работа может привести
|
||||
к дублю задачи в Jira, к команде, зависшей в `claimed` без пути восстановления, или к лишним запросам к Jira банка:
|
||||
|
||||
- **R-11 / O-9** — хаб не знает логин Jira агента, поэтому `ensure_issue` никогда не подтверждается по состоянию; каждый
|
||||
потерянный отчёт о создании задачи требует ручного действия.
|
||||
- **O-3** — связь симметричного типа («Связанные»), стоящая в Jira в обратную сторону, считается конфликтом в
|
||||
`check_confirmed` и «отсутствующей» в отчёте импорта (13.10.2026 импорт показал 3 такие ложные `missing_links`).
|
||||
- **M-2** — ответ на `claim` потерян (сбой папки, перезапуск агента во время долгого запроса): команда навсегда `claimed`,
|
||||
в журнале агента её нет; очередь стоит 10 минут, затем команда требует force-cancel или повтора.
|
||||
- **R-11 / O-9** — потерянный отчёт `ensure_issue` не закрывается подтверждением по состоянию (хаб не знает логин Jira
|
||||
агента); каждый такой случай требует ручного действия.
|
||||
- **M-5** — поздний отчёт по force-отменённой `ensure_issue` теряет ключ созданной задачи.
|
||||
- **M-11** — отчёт по команде в `queued` принимается; две `ensure_issue` с одной темой в пакете дают одну задачу на два
|
||||
временных ключа.
|
||||
- **O-3** — связь симметричного типа («Связанные»), стоящая в Jira в обратную сторону, попадает в `missing_links` отчёта
|
||||
импорта (13.10.2026 — 3 ложные тревоги).
|
||||
- **M-9 / O-2** — после перезапуска агент забывает время последнего вызова Jira и сразу выполняет команду без паузы темпа;
|
||||
при восстановлении по журналу пауза обнуляется явно.
|
||||
- **FM-4** — восстановление по журналу повторно пишет `claimed`.
|
||||
- Расхождение со спекой: `agent_status` отдаёт признак `online`, спека говорит `offline`.
|
||||
|
||||
Решения владельца 13.10.2026 (сессия brainstorming): пакет A follow-ups — первым; вариант A для M-2 — хаб довыдаёт
|
||||
команду, выданную агенту и оставшуюся без отчёта (принцип «хаб не повторяет команды» уточняется: довыдача
|
||||
неподтверждённой выдачи — не повтор); дизайн частей 1–3 (повторная выдача и отчёты; факты агента о Jira в claim; темп
|
||||
после перезапуска, `online`, тесты и выкладка) одобрен.
|
||||
Решения владельца 13.10.2026 (сессия brainstorming):
|
||||
- пакет A follow-ups — первым;
|
||||
- M-2, вариант A: хаб довыдаёт команду, выданную агенту и оставшуюся без отчёта (принцип «хаб не повторяет команды»
|
||||
уточняется: новых команд хаб не создаёт, довыдача неподтверждённой выдачи — доставка, не повтор);
|
||||
- после написания спеки, при планировании: довыдача закрывает и сценарий R-11 (потерянная `ensure_issue` выдаётся снова,
|
||||
агент находит свою задачу по `reporter = currentUser()`), а подтверждение по состоянию при довыдаче почти недостижимо
|
||||
(`refresh` не выдаётся, пока есть `claimed`). Поэтому передача логина агента и симметричных типов связей в `claim`
|
||||
из change убрана; R-11 закрывается довыдачей; O-3 — отдельным списком связей «в обратную сторону» в отчёте импорта.
|
||||
|
||||
## What Changes
|
||||
|
||||
@ -29,31 +33,34 @@
|
||||
- повторная выдача: `claim` при наличии команды в `claimed` выдаёт её снова (самую старую), счётчик `deliveries`;
|
||||
после 3 выдач без отчёта — `failed` «агент не отчитался после 3 выдач»; логика «свежей claimed» (10 минут простоя) и
|
||||
обход пакета с зависшим определителем (R-13) удаляются;
|
||||
- миграция схемы v1 → v2 (`command.deliveries`) с копией базы перед ней — первая миграция живой базы;
|
||||
- отчёт по `queued` — 409; пакет с двумя `ensure_issue` одной темы — 422;
|
||||
- поздний отчёт по отменённой `ensure_issue` с ключом сохраняет результат и переносит заметку `new:N` → ключ, статус
|
||||
остаётся `cancelled`;
|
||||
- `agent_status` хранит `jira_user` и `symmetric_link_types` из claim; `check_confirmed` подтверждает `ensure_issue`
|
||||
задачей от `jira_user` и не считает обратную связь симметричного типа конфликтом; отчёт импорта так же;
|
||||
- заготовка настройки `reporter` удаляется; MCP `agent_status` показывает логин агента и довыданные команды.
|
||||
- отчёт импорта: связи из `links.csv`, которые в зеркале есть того же типа в обратную сторону, — в `reversed_links`,
|
||||
а не в `missing_links`;
|
||||
- заготовка настройки `reporter` удаляется; `agent_status` — `redelivered` вместо `stuck`; MCP показывает довыданные
|
||||
команды; принцип повторов уточняется в `CLAUDE.md`, `ARCHITECTURE.md`, docstring `queue`.
|
||||
- **jira-agent** (версия 3):
|
||||
- при старте `GET /myself` и `GET /issueLinkType`; `jira_user` и `symmetric_link_types` в каждом claim;
|
||||
- время последнего вызова Jira восстанавливается при старте из журнала; при восстановлении пауза команды не обнуляется;
|
||||
- при восстановлении `claimed` повторно не пишется.
|
||||
- Спеки: `hub-api`, `data-model`, `commands`, `agent`, `mcp` — MODIFIED; признак `online` закрепляется в спеке вместо
|
||||
`offline`.
|
||||
- Спеки: `hub-api`, `data-model`, `commands`, `agent`, `mcp`, `migration` — MODIFIED; признак `online` закрепляется в
|
||||
спеке вместо `offline`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Передача логина Jira агента и симметричных типов связей в хаб (см. Why).
|
||||
- Остальные находки follow-ups (`T-n`, `M-n` вне перечня, расхождения второго порядка) — пакеты C и D.
|
||||
- Проверки поведения на реальной Jira банка (CRLF, Epic Link, постраничность комментариев, поле спринта) — пакет B.
|
||||
- Подъём `min_agent_version` до 3: агент v2 продолжает работать, новые поля claim необязательны.
|
||||
- Подъём `min_agent_version` до 3: агент v2 продолжает работать.
|
||||
- Дубли `ensure_issue` между разными пакетами: поиск дубля по теме за 7 дней уже сводит их к одной задаче.
|
||||
|
||||
## Impact
|
||||
|
||||
- `jira-hub`: `queue.py` (claim, report_result, agent_view), `commands.py` (validate, check_confirmed), `importer.py`,
|
||||
`db.py` (миграция: `command.deliveries`, `agent_status.jira_user`, `agent_status.symmetric_link_types`), `api.py`
|
||||
(409 на отчёт по queued), `settings.py`, `mcp_server.py`; `CLAUDE.md` и `ARCHITECTURE.md` — уточнённый принцип повторов.
|
||||
- `jira-agent`: `src/agent.py` (Runner, Journal, HubClient.claim), `README.md`, версия 3.
|
||||
- Контракт: тело `POST /agent/claim` получает два необязательных поля; ответы не меняются.
|
||||
- `jira-hub`: `queue.py` (claim, report_result, agent_view, maybe_auto_refresh, reconcile), `commands.py` (validate,
|
||||
check_confirmed), `importer.py`, `db.py` (миграция v2), `api.py` (409 на отчёт по queued), `mcp_server.py`;
|
||||
`CLAUDE.md`, `ARCHITECTURE.md`.
|
||||
- `jira-agent`: `src/agent.py` (Runner, recover, execute), `README.md`, версия 3.
|
||||
- Контракт хаб ↔ агент: тела запросов не меняются; новый ответ 409 на отчёт по `queued` (агент уже трактует 409 как
|
||||
«закрыто»).
|
||||
- Issues: aep/jira-hub#9, aep/jira-agent#9.
|
||||
|
||||
@ -8,24 +8,10 @@
|
||||
выдачу, см. `commands`). При изменении `settings_version` в ответе `claim` агент SHALL перечитать настройки до выполнения
|
||||
команды.
|
||||
|
||||
Факты о Jira: после первой загрузки настроек и до первого `claim` агент SHALL вызвать `GET /myself` и
|
||||
`GET /issueLinkType` и передавать в каждом `claim` `jira_user` (`name` из `/myself`) и `symmetric_link_types` (имена типов
|
||||
с `inward == outward`). Если вызов не удался, агент SHALL продолжить работу без этого поля и повторять сбор перед
|
||||
`claim` не чаще раза в 10 минут, пока не получит значение.
|
||||
|
||||
#### Scenario: Остановка между командами
|
||||
- **WHEN** получен Ctrl-C во время выполнения команды
|
||||
- **THEN** текущая команда доделывается и отчёт отправляется, затем процесс завершается
|
||||
|
||||
#### Scenario: Факты о Jira в claim
|
||||
- **WHEN** агент стартовал, `/myself` вернул `name = "bridge-user"`, в `/issueLinkType` у «ASUPR_Связанные»
|
||||
`inward == outward`
|
||||
- **THEN** каждый `claim` содержит `jira_user = "bridge-user"` и `symmetric_link_types`, включающий «ASUPR_Связанные»
|
||||
|
||||
#### Scenario: Jira недоступна при старте
|
||||
- **WHEN** `/myself` при старте отвечает ошибкой соединения
|
||||
- **THEN** агент работает, `claim` без `jira_user`; через 10 минут агент повторяет `/myself`
|
||||
|
||||
### Requirement: Журнал на диске и восстановление
|
||||
Агент SHALL вести `journal.jsonl` (только дозапись) рядом с собой: строки `claimed {command_id, at}` (сразу после
|
||||
получения команды, до паузы), `started {command_id, at}`, `finished {command_id, status, result, error, jira_calls,
|
||||
@ -62,8 +48,8 @@ rate_limited, at}`, `acked {command_id, at}`. При старте агент SHA
|
||||
### Requirement: Доставка и запуск
|
||||
README агента SHALL описывать: копирование `agent.py` и `agent.example.json` через общую папку, заполнение `agent.json`
|
||||
(включая выбор транспорта в `hub_url`), `check`, запуск в терминале и через `nohup` с логом, опциональный launchd-plist,
|
||||
`prune`. Версия агента SHALL быть в константе файла (3 — с фактами о Jira в `claim` и восстановлением темпа) и в каждом
|
||||
отчёте; хаб предупреждает в `agent_status`, если версия ниже минимальной из настроек.
|
||||
`prune`. Версия агента SHALL быть в константе файла (3 — с восстановлением темпа после перезапуска) и в каждом отчёте;
|
||||
хаб предупреждает в `agent_status`, если версия ниже минимальной из настроек.
|
||||
|
||||
#### Scenario: Устаревший агент
|
||||
- **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2`
|
||||
|
||||
@ -1,50 +1,5 @@
|
||||
## MODIFIED 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`), `GET /issueLinkType` (симметричность) | нужная связь есть — ничего; нет нужной и нет конфликта — `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). Тип связи симметричен, если в Jira у него
|
||||
`inward == outward`; для симметричного типа связь `target→source` равнозначна `source→target` («нужная связь есть»).
|
||||
|
||||
#### 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_link {source: AURORA-1445, target: AURORA-1446, link_type: "связанные"}`, тип симметричен, в Jira
|
||||
есть «Связанные» 1446→1445
|
||||
- **THEN** агент ничего не пишет, отчёт `done` с `existed: true`
|
||||
|
||||
#### Scenario: Переход по пути
|
||||
- **WHEN** `ensure_status {key: X, target: ["Разработка", "Готово"]}` и X в «Ожидание разработки»
|
||||
- **THEN** два `POST /transitions` с паузой между ними; если X уже «Готово» — `done` без записи
|
||||
|
||||
#### Scenario: Создание с проверкой дубля
|
||||
- **WHEN** `ensure_issue` повторяется после падения агента, а задача с той же темой от нас уже создана час назад
|
||||
- **THEN** агент не создаёт вторую, отчёт `done` с ключом найденной
|
||||
|
||||
### Requirement: Пакет и порядок выполнения
|
||||
Пакет SHALL создаваться одним вызовом с автором, заголовком и списком команд; хаб SHALL назначить `seq` по порядку списка
|
||||
и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh`). Пакет с двумя и более
|
||||
@ -56,6 +11,10 @@
|
||||
- **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 записываться в `error` (HTTP-код, текст Jira до 1500 символов), пакет продолжается.
|
||||
|
||||
@ -64,23 +23,18 @@
|
||||
сначала выдать снова самую старую (по `claimed_at`, затем `id`) команду в `claimed`: тот же `id` и параметры,
|
||||
`pause_seconds` по текущей политике темпа, новый `claimed_at`, `deliveries + 1`. Если у неё уже `deliveries >= 3`, хаб SHALL
|
||||
перевести её в `failed` с `error = "агент не отчитался после 3 выдач"` и перейти к следующей (довыдача другой `claimed`
|
||||
или следующая `queued`). Довыдача — доставка той же выдачи, а не повтор команды.
|
||||
или следующая `queued`). Довыдача — доставка той же выдачи, а не повтор команды: новых команд хаб MUST NOT создавать
|
||||
сам, повтор после `failed` — новая команда с теми же параметрами по запросу интерфейса.
|
||||
|
||||
**Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб SHALL
|
||||
перевести её в `confirmed_by_state`:
|
||||
- `ensure_issue` — задача с той же `summary`, `reporter = agent_status.jira_user`, созданная не раньше 7 дней назад; пока
|
||||
`jira_user` неизвестен — не подтверждается;
|
||||
- `ensure_fields` — все поля совпадают; `ensure_status` — статус равен последнему в пути; `ensure_sprint` — спринт совпадает;
|
||||
- `ensure_link` — нужная связь есть (`source→target` нужного типа или, для типа из `agent_status.symmetric_link_types`,
|
||||
`target→source` того же типа) и нет конфликта (связи другого типа из `link_types` между теми же задачами в любом
|
||||
направлении, обратной связи несимметричного типа).
|
||||
Для `ensure_comment` и `refresh` подтверждения по состоянию нет: такая команда ждёт отчёта агента, довыдачи или отмены
|
||||
человеком. Новых команд хаб 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`
|
||||
@ -90,12 +44,7 @@
|
||||
- **WHEN** команда 41 в `claimed` с `deliveries = 3`, агент снова прислал `claim`, в очереди есть команда 42
|
||||
- **THEN** 41 — `failed` «агент не отчитался после 3 выдач»; `claim` отдаёт 42
|
||||
|
||||
#### Scenario: Создание подтверждается по состоянию
|
||||
- **WHEN** `ensure_issue {summary: "Шахматка: комментарии"}` в `claimed`, `agent_status.jira_user = "bridge-user"`,
|
||||
`refresh` загрузил задачу с этой темой, `reporter = "bridge-user"`, созданную сегодня
|
||||
- **THEN** команда `confirmed_by_state` с ключом задачи; заметка `new:N` перенесена на ключ
|
||||
|
||||
#### Scenario: Связь симметричного типа в обратную сторону
|
||||
- **WHEN** `ensure_link {source: A, target: B, link_type: "связанные"}` в `claimed`, тип в `symmetric_link_types`, в
|
||||
зеркале есть только «Связанные» B→A
|
||||
- **THEN** команда `confirmed_by_state`
|
||||
#### Scenario: Потерянный отчёт о создании задачи
|
||||
- **WHEN** `ensure_issue {temp_key: new:5, summary: "Отчёт"}` выполнена агентом, отчёт не дошёл, агент прислал `claim`
|
||||
- **THEN** команда выдаётся снова; агент находит созданную задачу поиском дубля и отчитывается `done` с её ключом; второй
|
||||
задачи нет, заметка `new:5` перенесена на ключ
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
### Requirement: Схема базы хаба
|
||||
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_note`, `batch`, `command`,
|
||||
`sync_run`, `agent_status`, `setting`. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру версии
|
||||
в `setting.schema_version` (текущая — 2). Миграция существующей базы SHALL выполняться по шагам версий в одной транзакции;
|
||||
перед миграцией хаб SHALL скопировать файл базы в `backups/<дата-время>-schema-v<старая>.sqlite`. Денежных и дробных
|
||||
в `setting.schema_version` (текущая — 2). Миграция существующей базы SHALL выполняться в одной транзакции; перед ней
|
||||
хаб SHALL скопировать файл базы в `backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой. Денежных и дробных
|
||||
полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени).
|
||||
|
||||
#### Scenario: Первый запуск
|
||||
@ -13,8 +13,8 @@
|
||||
|
||||
#### Scenario: Миграция базы v1
|
||||
- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed`
|
||||
- **THEN** в `backups/` копия базы v1; добавлены `command.deliveries` (у `claimed` — 1, у остальных — 0),
|
||||
`agent_status.jira_user`, `agent_status.symmetric_link_types`; данные на месте; `schema_version = 2`
|
||||
- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); данные на месте;
|
||||
`schema_version = 2`
|
||||
|
||||
### Requirement: Служебные заметки
|
||||
Таблица `issue_note` SHALL хранить поля, которых нет в Jira: `key` (PK; ключ Jira или временный `new:<n>`),
|
||||
@ -41,11 +41,9 @@ SHALL вычисляться из команд, не храниться.
|
||||
### 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`, `jira_user`
|
||||
(логин Jira агента, присланный в `claim`), `symmetric_link_types` (JSON-список имён симметричных типов связей Jira,
|
||||
присланный в `claim`). `jira_user` — сведения о среде агента, а не настройка. Через API и MCP `agent_status` SHALL
|
||||
отдаваться с признаком `online` (истина, если `last_seen_at` моложе 5 минут) и списком `redelivered` — команд в
|
||||
`claimed`, выданных больше одного раза (`id`, `type`, `claimed_at`, `deliveries`).
|
||||
хранить `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 минут
|
||||
|
||||
@ -2,17 +2,16 @@
|
||||
|
||||
### Requirement: Маршруты агента
|
||||
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
|
||||
- `POST /agent/claim` `{agent_version, settings_version, last_jira_call_at, jira_user?, symmetric_link_types?}` — долгий
|
||||
запрос до 30 с; ответ `{command: {id, type, params, pause_seconds}, settings_version}` или `{command: null,
|
||||
settings_version}`; обновляет `agent_status` (непустые `jira_user` и `symmetric_link_types` записываются, отсутствующие
|
||||
не стирают прежние значения). Если есть команда в `claimed`, `claim` SHALL выдать её снова (повторная выдача, см.
|
||||
`commands`), а не следующую из очереди.
|
||||
- `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 (для восстановления из журнала), включая `pause_seconds`.
|
||||
- `POST /agent/commands/{id}/result` `{status, result, error, jira_calls, rate_limited}` — идемпотентно: повторный
|
||||
отчёт по команде в терминальном статусе отвечает 200 без изменений, кроме позднего отчёта `done` с `result.key` по
|
||||
отменённой `ensure_issue` (результат записывается, статус остаётся `cancelled`, заметка `new:N` переносится на ключ).
|
||||
Отчёт по команде в `queued` SHALL отвергаться 409 «команда не выдана».
|
||||
отменённой `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` — ключи зеркала (для определения соседей).
|
||||
@ -27,12 +26,7 @@
|
||||
|
||||
#### Scenario: Отчёт по невыданной команде
|
||||
- **WHEN** `POST /agent/commands/16/result` с `status = done`, команда 16 в `queued`
|
||||
- **THEN** 409, команда осталась `queued`
|
||||
|
||||
#### Scenario: Факты агента в claim
|
||||
- **WHEN** агент v3 прислал `claim` с `jira_user = "bridge-user"` и `symmetric_link_types = ["ASUPR_Связанные"]`, затем
|
||||
агент v2 прислал `claim` без этих полей
|
||||
- **THEN** `agent_status.jira_user` и `symmetric_link_types` сохранили значения от первого `claim`
|
||||
- **THEN** 409 `not_claimed`, команда осталась `queued`
|
||||
|
||||
### Requirement: Маршруты интерфейсов
|
||||
Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном:
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
| `batches` | `limit` | последние пакеты со статусом |
|
||||
| `command_retry`, `batch_cancel` | `id` | новый пакет / результат отмены |
|
||||
| `refresh` | `keys` или `full`, `wait` (bool) | `sync_run` или id пакета |
|
||||
| `agent_status` | — | онлайн/офлайн, версия, логин Jira агента, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек |
|
||||
| `agent_status` | — | онлайн/офлайн, версия, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек |
|
||||
| `sync_log` | `limit` | последние `sync_run` |
|
||||
| `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` |
|
||||
|
||||
@ -27,5 +27,5 @@
|
||||
- **THEN** ответ через 6 минут со статусами всех команд
|
||||
|
||||
#### Scenario: Статус агента с довыдачей
|
||||
- **WHEN** агент v3 на связи, команда 40 выдана дважды и ещё без отчёта
|
||||
- **THEN** `agent_status` — «агент онлайн (версия 3, Jira: bridge-user, …)» и строка о команде 40 с числом выдач 2
|
||||
- **WHEN** агент на связи, команда 40 выдана дважды и ещё без отчёта
|
||||
- **THEN** ответ `agent_status` содержит «агент онлайн» и строку о команде 40 с числом выдач 2
|
||||
|
||||
19
openspec/changes/bridge-reliability/specs/migration/spec.md
Normal file
19
openspec/changes/bridge-reliability/specs/migration/spec.md
Normal file
@ -0,0 +1,19 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Импорт CSV-пакета
|
||||
`jira-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.csv` и заполнить
|
||||
`issue_note` (ключ Jira; для строк без ключа — `new:<номер строки>`): статус по факту, вид работы, что сделано, что
|
||||
осталось, внутренние колонки. `links.csv` SHALL импортироваться как сверочный набор в отчёт импорта (ожидаемые связи
|
||||
против зеркала после первого `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`, и связи, которые есть в обратную сторону
|
||||
|
||||
#### Scenario: Связь в обратную сторону
|
||||
- **WHEN** в `links.csv` строка «AURORA-1445 → AURORA-1446, связанные», а в зеркале есть только «ASUPR_Связанные»
|
||||
1446→1445
|
||||
- **THEN** связь в `reversed_links`, в `missing_links` её нет
|
||||
Loading…
x
Reference in New Issue
Block a user