docs(openspec): agent-self-update — proposal, design, дельты agent/hub-api/mcp (#22)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3931a88ce8
commit
ce148b69f9
2
openspec/changes/agent-self-update/.openspec.yaml
Normal file
2
openspec/changes/agent-self-update/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-14
|
||||
194
openspec/changes/agent-self-update/design.md
Normal file
194
openspec/changes/agent-self-update/design.md
Normal file
@ -0,0 +1,194 @@
|
||||
## Context
|
||||
|
||||
Мост в работе: хаб на Mac mini (launchd, схема v2), агент v4 на виртуалке банка через общую папку UTM
|
||||
(`hub_url = folder:/Volumes/My Shared Files/RSHB/jira-bridge`), запущен владельцем в терминале из `~/jira-agent-bundle`.
|
||||
Код: хаб `main` 3931a88, агент `main` 96c92d9. Параллельно идёт `web-app` (#21, ветка `feat/21-web-app`): из бэкенда
|
||||
она трогает только `api.py` (раздача приложения) и требование «Процесс и доступ» спеки `hub-api`.
|
||||
|
||||
Найдено при чтении кода:
|
||||
- `agent.py`: `HERE = dirname(abspath(__file__))` — `agent.json` и `journal.jsonl` ищутся рядом с запущенным файлом,
|
||||
значит `python3 agent.py.new selftest` в том же каталоге читает тот же `agent.json`.
|
||||
- `acquire_lock` открывает `<журнал>.lock` через `open()` — fd не наследуется дочерними процессами (PEP 446);
|
||||
блокировка `flock` освобождается при выходе процесса.
|
||||
- `Runner.loop`: после `claim` без команды — `idle += 1`, `sleep(poll)`. Это единственная точка, где у агента нет
|
||||
команды в полёте и нет неподтверждённого отчёта.
|
||||
- `cmd_check` делает одно чтение `GET /myself` в Jira и `GET /agent/settings` в хаб.
|
||||
- Хаб: `h_claim` отдаёт `{command, settings_version}`; `GET /agent` — `queue.agent_view(conn)`; MCP `agent_status`
|
||||
форматирует его `fmt_agent`. Тело запроса ограничено 16 МБ; ответы реле — строка JSON в файле.
|
||||
- Python, упавший на необработанном исключении или `SyntaxError`, выходит с кодом 1; `argparse` при ошибке — 2.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** после одного последнего ручного обновления агент обновляется сам после `jira-hub agent publish`; плохая
|
||||
версия не останавливает мост (не ставится или откатывается); правило «одна команда в полёте», журнал и блокировка не
|
||||
нарушаются; хаб без миграции схемы; совместимость в обе стороны со старыми хабом и агентом.
|
||||
|
||||
**Non-Goals:** см. proposal.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Хаб
|
||||
|
||||
1. **Публикация.** `jira-hub agent publish PATH`:
|
||||
- читает файл как UTF-8 (иначе ошибка), размер не больше 1 МБ;
|
||||
- версия — из единственной строки вида `AGENT_VERSION = <целое>` (регулярное выражение по строкам; нет или
|
||||
несколько — ошибка);
|
||||
- версия MUST быть больше опубликованной (если есть), иначе ошибка «опубликована v<N>, новая должна быть больше»;
|
||||
- sha256 — от байтов файла;
|
||||
- запись в `<JIRA_HUB_HOME>/release/` (каталог 0700): `agent.py` через временный файл и `os.replace`, затем
|
||||
`manifest.json` `{version, sha256, size, published_at}` так же; манифест пишется последним — по нему хаб считает
|
||||
версию опубликованной;
|
||||
- вывод: «опубликована v<N>, sha256 <первые 12>…, <size> байт»; код 0. Ошибки — сообщение и код 1.
|
||||
Работает без запущенного `serve`: сервер читает манифест при каждом обращении.
|
||||
2. **Чтение публикации** (`release.py`): `current(home) -> {version, sha256} | None` — манифест; битый или неполный
|
||||
манифест — `None` и предупреждение в лог (агенту нечего обновлять, это безопасно). `load(home) -> {version, sha256,
|
||||
content}` — манифест и файл; sha256 файла пересчитывается, несовпадение — `None` и ошибка в лог.
|
||||
3. **`claim`.** Ответ дополняется `agent_release: current(home)` всегда, независимо от версии агента — решает агент.
|
||||
Тело `claim` принимает необязательные `update_state` (объект или `null`) и `launcher_version` (целое или `null`);
|
||||
хаб сохраняет последние значения в памяти процесса (`Hub.agent_update`) при каждом `claim`; значения неверного типа
|
||||
игнорируются (claim не отвергается). В `update_state.error` хаб хранит не больше 500 символов.
|
||||
4. **`GET /agent/release`** (агентский токен): `200 {version, sha256, content}`; нет публикации или файл не сходится с
|
||||
манифестом — `404 {error: "no_release"}`.
|
||||
5. **`GET /agent`** дополняется полями `release` (`current` + `published_at`), `update_state`, `launcher_version`
|
||||
(последние из памяти; после перезапуска хаба — `null` до первого `claim`) и `update_hint`:
|
||||
- `release.version > agent_version`, агент онлайн и `launcher_version = null` → «агент запущен без run.py —
|
||||
обновится только вручную»;
|
||||
- `update_state.state = "rolled_back"` → «откат с v<to> на v<from>: <error>»;
|
||||
- `update_state.state = "rejected"` → «v<to> отвергнута: <error>»;
|
||||
- иначе `null`.
|
||||
6. **MCP `agent_status`** выводит дополнительно строку «публикация: v<N> от <дата>» (или «публикации нет»), строку
|
||||
«обновление: <state> v<from> → v<to>» при наличии `update_state`, «запускатель: v<N>» / «без запускателя» и
|
||||
`update_hint`, если не `null`.
|
||||
|
||||
### Агент
|
||||
|
||||
7. **Состояние обновления — `update.json`** рядом с `agent.py`: `{state, from_version, to_version, sha256, error, at}`.
|
||||
Пишется атомарно (временный файл + `os.replace`). Состояния:
|
||||
- `staged` — агент проверил `agent.py.new` и выходит с кодом 75 (пишет агент);
|
||||
- `trial` — запускатель подменил файл и запустил новую версию (пишет запускатель);
|
||||
- `ok` — новая версия получила первый успешный ответ `claim` (пишет агент, только из состояния `trial` со своей
|
||||
версией = `to_version`);
|
||||
- `rolled_back` — запускатель вернул прежнюю версию (пишет запускатель, `error` — «код <N> через <сек> с» или
|
||||
«не вышла на связь за 10 мин»);
|
||||
- `rejected` — агент отверг версию до подмены (`error` — причина).
|
||||
Нет файла — `null`. Агент передаёт содержимое в каждом `claim` как `update_state`.
|
||||
8. **Когда проверять.** Только в `Runner.loop` после ответа `claim` с `command = null`, если `stop` не выставлен,
|
||||
`auto_update` не `false` и `agent_release.version > AGENT_VERSION`. Пропуск:
|
||||
- `update.json` в `rejected`/`rolled_back` с тем же `to_version` и `sha256` — версия уже отвергнута;
|
||||
- запуск без запускателя (нет переменной окружения `JIRA_AGENT_LAUNCHER`) — строка в лог «доступна v<N>; для
|
||||
самообновления запускайте через run.py», не чаще раза в час;
|
||||
- предыдущая попытка этой версии закончилась временным сбоем меньше 5 минут назад.
|
||||
9. **Подготовка версии.**
|
||||
1. `GET /agent/release`; `ConnectionError`/`HttpError` — временный сбой.
|
||||
2. `version`/`sha256` ответа MUST совпасть с `agent_release` из `claim` (иначе временный сбой — публикация сменилась
|
||||
между запросами), sha256 от `content.encode("utf-8")` MUST совпасть с `sha256` (иначе `rejected`
|
||||
«sha256 не сходится»).
|
||||
3. `compile(content, "agent.py.new", "exec")` — `SyntaxError` → `rejected`; строка `AGENT_VERSION = <N>` MUST
|
||||
дать `N = version` → иначе `rejected`.
|
||||
4. Запись `agent.py.new` (временный файл + `os.replace`, права как у `agent.py`).
|
||||
5. `python3 agent.py.new selftest` (`sys.executable`, тот же каталог, таймаут 120 с): код 0 — годен; 3 — временный
|
||||
сбой (хаб или Jira недоступны) или таймаут; любой другой — `rejected` «selftest: код <N>» с последними 500
|
||||
символами вывода.
|
||||
6. Годен: `update.json` = `staged`, лог «v<N> готова, перезапуск через run.py», выход из цикла и `run` с кодом 75.
|
||||
При `rejected` и временном сбое `agent.py.new` удаляется; при `rejected` пишется `update.json`; агент продолжает
|
||||
работу. Сигнал остановки во время подготовки — `agent.py.new` удаляется, обычный выход.
|
||||
10. **`selftest`.** `python3 agent.py selftest`: печатает «jira-agent v<N>», выполняет то же, что `check`; код 0 —
|
||||
хаб и Jira доступны; 3 — что-то недоступно. Блокировку журнала не берёт, в Jira не пишет. Контракт кодов (0/3)
|
||||
MUST сохраняться в следующих версиях: его вызывает предыдущая версия.
|
||||
11. **Первый успешный `claim`** новой версии: если `update.json` = `trial` и `to_version = AGENT_VERSION` —
|
||||
записать `ok`, лог «обновление до v<N> подтверждено».
|
||||
12. **`auto_update`** — необязательное поле `agent.json`, по умолчанию `true`; `false` — агент не проверяет
|
||||
публикацию и не пишет подсказку.
|
||||
13. **Версия 5.** `AGENT_VERSION = 5`; `min_agent_version` хаба не поднимается.
|
||||
|
||||
### Запускатель `run.py`
|
||||
|
||||
14. **Запуск.** `python3 run.py [аргументы]` из любого каталога: работает с `agent.py`, `update.json` и т. д. в каталоге
|
||||
самого `run.py`; аргументы передаются `agent.py run` как есть (`--config`, `--journal`). Константа
|
||||
`LAUNCHER_VERSION = 1`. Своя блокировка `run.py.lock` (`flock`, неблокирующе): занята — «запускатель уже работает»,
|
||||
код 1.
|
||||
15. **Дочерний процесс.** `[sys.executable, "-u", <каталог>/agent.py, "run", *аргументы]` с переменной окружения
|
||||
`JIRA_AGENT_LAUNCHER=<LAUNCHER_VERSION>`; stdout/stderr наследуются (лог агента и запускателя — в одном месте).
|
||||
Агент передаёт `launcher_version` в `claim`. Строки запускателя — с префиксом «run:» и временем, как у агента.
|
||||
16. **Сигналы.** SIGINT запускатель игнорирует (Ctrl-C в терминале и так приходит дочернему процессу — общая группа).
|
||||
SIGTERM (`kill`, `launchctl unload`) пересылается дочернему процессу; повторный SIGTERM — пересылается снова
|
||||
(агент выходит немедленно, 130). После выхода дочернего процесса по сигналу запускатель выходит.
|
||||
17. **Коды выхода дочернего процесса:**
|
||||
- `0` или `130` — остановка человеком: запускатель выходит с тем же кодом;
|
||||
- `75` — обновление: если `update.json` = `staged` и sha256 `agent.py.new` совпадает — копия `agent.py` в
|
||||
`agent.py.prev` (временный файл + `os.replace`), затем `os.replace(agent.py.new, agent.py)` (файл `agent.py`
|
||||
существует всегда), `update.json` = `trial` `{from_version, to_version, sha256, at}`, запуск новой версии.
|
||||
Нет `staged` или sha не сходится — `agent.py.new` удаляется, `update.json` = `rejected` «файл изменился после
|
||||
проверки», запуск текущей версии;
|
||||
- иное — падение: в пробный период — откат (п. 18), иначе — перезапуск той же версии с паузой (п. 19).
|
||||
18. **Пробный период.** Пока `update.json` = `trial`, запускатель раз в секунду читает его. Новая версия здорова,
|
||||
когда агент записал `ok`. Откат, если дочерний процесс упал (код не 0/130/75) или за 10 минут не стал здоровым
|
||||
(тогда SIGTERM, через 60 с SIGKILL): `agent.py.prev` → `agent.py` (`os.replace`), `update.json` = `rolled_back`,
|
||||
лог «run: откат v<to> → v<from>: <причина>», запуск прежней версии без паузы. Остановка человеком в пробный период
|
||||
(0/130) — выход; при следующем старте `run.py` с `trial` пробный период начинается заново.
|
||||
19. **Перезапуск после падения.** Пауза 5 с, удваивается до 300 с при каждом следующем падении; если процесс проработал
|
||||
не меньше 10 минут, пауза сбрасывается до 5 с. Лог «run: агент упал (код <N>), перезапуск через <S> с». Ctrl-C или
|
||||
SIGTERM во время паузы — выход.
|
||||
20. **Старт запускателя.** `update.json` = `staged` и есть `agent.py.new` — подмена, как по коду 75 (запускатель
|
||||
остановили между выходом агента и подменой). Лишний `agent.py.new` без `staged` — удаляется. `agent.py` отсутствует, а `agent.py.prev`
|
||||
есть — восстановить из `.prev` (защита от ручной ошибки; штатная подмена `agent.py` не удаляет).
|
||||
|
||||
### Выкатка и процесс
|
||||
|
||||
21. **Совместимость.** Старый хаб: нет `agent_release` в ответе — агент v5 не обновляется; лишние поля в теле `claim`
|
||||
старый хаб игнорирует (`body.get`). Агент v4 с новым хабом поле `agent_release` игнорирует.
|
||||
22. **Последнее ручное обновление.** После merge хаба (pull + kickstart) — на виртуалке остановить v4, положить
|
||||
`agent.py` v5 и `run.py` в `~/jira-agent-bundle`, `python3 agent.py check`, запуск `python3 run.py`. При launchd —
|
||||
в plist `run.py` вместо `agent.py`.
|
||||
23. **Публикация в процессе.** В чеклист закрытия PR агента (CLAUDE.md агента) добавляется пункт: после merge
|
||||
`uv run --project <jira-hub> jira-hub agent publish <jira-agent>/src/agent.py` и проверка `agent_status` через
|
||||
несколько минут (`update_state = ok`, версия новая).
|
||||
24. **Живая проверка** (сценарий в `awp-ca/admin/`): публикация v6 (только номер) — агент обновился, `ok`; v7, падающая
|
||||
на старте, — откат на v6, `rolled_back`, повторно не ставится; v8 = код v6 — ставится.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Код на банковской виртуалке без человека.** Кто владеет Mac mini (каталог данных хаба, токены) — ставит код на
|
||||
виртуалку. Сейчас то же самое: владелец копирует файл, лежащий на Mac mini. sha256 защищает от битой передачи, не от
|
||||
подмены. Подпись не делается (R-9).
|
||||
- **Версия, проходящая selftest и первый `claim`, но ломающаяся позже** (например, на выполнении команды). Запускатель
|
||||
перезапустит её с паузой, откатом это не считается; видно в логе и по `agent_status` (команды `failed`). Лечится
|
||||
публикацией исправления — агент на связи и обновится.
|
||||
- **Ошибка в `run.py`** не лечится автоматически: файл маленький, меняется вручную, покрыт тестами на поддельных агентах.
|
||||
- **Пересечение с #21** — только `api.py`; ветка, сливаемая второй, ребейзится.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Merge хаба → `git pull` в `jira-hub`, `launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`; `GET /agent`:
|
||||
`release = null`.
|
||||
2. Merge агента → файлы `agent.py` v5 и `run.py` в корень `RSHB/` (ловушка VirtioFS: новую папку виртуалка может не
|
||||
увидеть); владелец на виртуалке: остановить v4, скопировать в `~/jira-agent-bundle`, `check`, `python3 run.py`;
|
||||
`agent_status`: версия 5, запускатель v1. Удалить файлы из `RSHB/`.
|
||||
3. Живая проверка (п. 24). Откат всей фичи: запуск `python3 agent.py` напрямую (без запускателя агент себя не
|
||||
обновляет), публикации не делать.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Владелец 14.10.2026 утвердил: доставку через хаб (A), запускатель с откатом (2), код только в каталоге `run.py` на
|
||||
виртуалке, части 1–3 дизайна в чате. Остальное — рулинги при написании спеки.
|
||||
|
||||
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|---|
|
||||
| R-1 (design #1) | 2026-10-14 | Публикация только вверх; снятия публикации и `--force` нет | агент двигается только вверх; откат — публикация старого кода с новым номером | для отката нужно поднять номер в файле — одна строка |
|
||||
| R-2 (design #1, #2) | 2026-10-14 | Публикация — файлы в `<данные>/release/`, без таблицы в БД | без миграции схемы и без пересечения с #21; CLI работает без `serve` | нет истории публикаций — она в git агента |
|
||||
| R-3 (design #3) | 2026-10-14 | `update_state` и `launcher_version` хаб держит в памяти, агент шлёт их в каждом `claim` | без миграции; после перезапуска хаба восстанавливаются за секунды | между перезапуском хаба и первым `claim` состояние обновления не видно |
|
||||
| R-4 (design #8) | 2026-10-14 | Проверка обновления только после `claim` без команды | единственная точка без команды в полёте и неподтверждённого отчёта | при непрерывной очереди обновление ждёт её опустошения |
|
||||
| R-5 (design #18) | 2026-10-14 | Здоровье новой версии — первый успешный `claim`; пробный период 10 мин; SIGTERM, через 60 с SIGKILL | `claim` без команды отвечает за секунды и проверяет связь с хабом, настройки и цикл | версия, ломающаяся только на командах, откатом не ловится (см. Risks) |
|
||||
| R-6 (design #19) | 2026-10-14 | Перезапуск после падения — пауза 5…300 с, сброс после 10 мин работы | не долбить хаб при постоянном падении, быстро подниматься при разовом | до 5 мин простоя при повторяющемся падении |
|
||||
| R-7 (design #9, #10) | 2026-10-14 | Компиляция и версия проверяются в старом процессе; `selftest` — коды 0/3, прочие — отказ | код 1 у Python — любое исключение, нельзя отличить «нет сети» от «битый файл» | новая версия, сломавшая контракт `selftest`, будет отвергнута — видно в `agent_status` |
|
||||
| R-8 (design #8) | 2026-10-14 | Временный сбой подготовки — повтор не чаще раза в 5 мин | папка или Jira могут быть недоступны минутами | обновление задерживается до 5 мин после восстановления связи |
|
||||
| R-9 (Risks) | 2026-10-14 | Подписи файла нет, только sha256 | доверие то же, что при ручном копировании с Mac mini; YAGNI | при компрометации Mac mini код на виртуалку ставится без человека |
|
||||
| R-10 (design #8) | 2026-10-14 | Без запускателя агент себя не обновляет, только пишет подсказку раз в час | выход с кодом 75 без запускателя остановил бы мост | нет |
|
||||
| R-11 (design #12) | 2026-10-14 | `auto_update` в `agent.json`, по умолчанию `true` | выключатель на стороне виртуалки, без публикации | нет |
|
||||
| R-12 (design #14, #15) | 2026-10-14 | `run.py` с `LAUNCHER_VERSION = 1`, своя блокировка, версия передаётся в `claim` | видно, где запускатель устарел; два запускателя не стартуют | нет |
|
||||
| R-13 (design #17) | 2026-10-14 | Код выхода 75 («обновление готово»), проверка sha `agent.py.new` ещё раз в запускателе | 75 = `EX_TEMPFAIL`, не пересекается с 0/1/2/130; повторная проверка ловит подмену файла между процессами | нет |
|
||||
| R-14 (design #3) | 2026-10-14 | `agent_release` в каждом ответе `claim`, решение — за агентом | хаб не знает, отвергнута ли версия; меньше логики в хабе | лишние ~80 байт в ответе |
|
||||
| R-15 (Context) | 2026-10-14 | Хаб правится параллельно с #21; дельта `hub-api` трогает только «Маршруты агента» и добавляет новое требование | у web-app — «Процесс и доступ» и раздача приложения; архивы не конфликтуют | конфликт в `api.py` при ребейзе — пара строк |
|
||||
| R-16 (design #15) | 2026-10-14 | stdout/stderr дочернего процесса наследуются, в `rolled_back` — только код и время | трейсбек остаётся в логе агента; захват вывода усложняет запускатель | причину отката видно в логе на виртуалке, в хабе — только код |
|
||||
| R-17 (design #13) | 2026-10-14 | Агент — версия 5, `min_agent_version` не поднимается | v4 продолжает работать до ручного обновления | нет |
|
||||
| R-18 (design #1) | 2026-10-14 | Публикация через CLI хаба, не через MCP | публикация ставит код на банковскую виртуалку — только явной командой в терминале | нет |
|
||||
65
openspec/changes/agent-self-update/proposal.md
Normal file
65
openspec/changes/agent-self-update/proposal.md
Normal file
@ -0,0 +1,65 @@
|
||||
## Why
|
||||
|
||||
Мост в ежедневной работе с 13.10.2026, агент меняется часто: v2 → v3 → v4 за два дня. Каждое обновление — ручная
|
||||
процедура на виртуалке банка: положить `agent.py` в корень общей папки, остановить старый процесс, скопировать файл
|
||||
в `~/jira-agent-bundle`, `check`, `run`. Хаб уже знает версию агента (`agent_version` в `claim`, предупреждение по
|
||||
`min_agent_version`), но доставлять код не умеет; падение агента никто не поднимает — он запущен в терминале.
|
||||
|
||||
Решения владельца 14.10.2026 (brainstorming):
|
||||
- доставка через хаб (вариант A): явная публикация `jira-hub agent publish`, версия в ответе `claim`, файл — по
|
||||
`GET /agent/release`; работает и по HTTP, и через общую папку;
|
||||
- перезапуск и откат — маленький постоянный запускатель `run.py` рядом с `agent.py` (вариант 2): подмена файла,
|
||||
пробный период, откат на прежнюю версию, перезапуск после падения;
|
||||
- код в общую папку не кладётся: агент получает файл через API хаба, пишет и запускает его в каталоге `run.py` на
|
||||
виртуалке (`~/jira-agent-bundle`);
|
||||
- хаб правится параллельно с `web-app` (#21), без пересечения требований спек;
|
||||
- после этого change ручное обновление на виртуалке — одно последнее (v5 + `run.py`); дальше агент обновляется сам
|
||||
после публикации, которую делает Claude в чеклисте закрытия PR агента.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **jira-hub**:
|
||||
- команда `jira-hub agent publish <agent.py>`: версия из строки `AGENT_VERSION = N`, sha256, только вверх; файл и
|
||||
`manifest.json` в `<данные>/release/` (права 0700); схема БД не меняется;
|
||||
- ответ `POST /agent/claim` — поле `agent_release: {version, sha256} | null`;
|
||||
- `POST /agent/claim` принимает `update_state` и `launcher_version`; хаб держит последние значения в памяти;
|
||||
- маршрут `GET /agent/release` (агентский токен) → `{version, sha256, content}`; 404 `no_release`;
|
||||
- `GET /agent` и MCP `agent_status`: опубликованная версия, состояние обновления, версия запускателя, подсказка
|
||||
«агент запущен без run.py».
|
||||
- **jira-agent** (версия 5):
|
||||
- новый файл `run.py` (Python 3.9, stdlib): дочерний `agent.py run`, код 75 — подмена файла, пробный период до первого
|
||||
успешного `claim` (10 мин), откат, перезапуск после падения с паузой 5…300 с, своя блокировка `run.py.lock`;
|
||||
- `agent.py`: в паузе между командами (ответ `claim` без команды) видит `agent_release` новее себя, скачивает,
|
||||
проверяет sha256, компиляцию и версию в файле, запускает `agent.py.new selftest`, выходит с кодом 75; отвергнутую
|
||||
версию не повторяет; без запускателя только пишет в лог раз в час; выключатель `auto_update` в `agent.json`;
|
||||
- новая команда `agent.py selftest`; `update.json` — состояние обновления рядом с `agent.py`;
|
||||
- README: запуск через `run.py`, последнее ручное обновление, публикация.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- нет -->
|
||||
|
||||
### Modified Capabilities
|
||||
- `agent`: состав (запускатель, `auto_update`, `selftest`), доставка и запуск; новые требования — самообновление и
|
||||
запускатель.
|
||||
- `hub-api`: маршруты агента (`agent_release`, `update_state`, `GET /agent/release`); новое требование — публикация
|
||||
версии агента.
|
||||
- `mcp`: `agent_status` показывает публикацию и состояние обновления.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- подпись файла ключом (целостность — sha256; доверие — как сейчас, см. design R-9);
|
||||
- снятие публикации и откат «вниз» по команде — откат делается публикацией старого кода с новой версией;
|
||||
- самообновление `run.py` — обновляется вручную, меняется редко;
|
||||
- изменение схемы БД хаба и `min_agent_version` (не поднимается);
|
||||
- показ публикации в веб-приложении (#21) — после его merge, отдельно.
|
||||
|
||||
## Impact
|
||||
|
||||
- jira-hub: `release.py` (новый), `cli.py`, `api.py` (`h_claim`, маршрут, `h_agent_get`), `queue.agent_view` не меняется,
|
||||
`mcp_server.fmt_agent`, README, ARCHITECTURE.md, CLAUDE.md. Пересечение с веткой `feat/21-web-app` — только `api.py`
|
||||
(разные места); дельта `hub-api` web-app трогает «Процесс и доступ», эта — «Маршруты агента».
|
||||
- jira-agent: `src/agent.py` (v5), `src/run.py` (новый), тесты, README, ARCHITECTURE.md, CLAUDE.md.
|
||||
- Выкатка: хаб — pull + kickstart; агент — последнее ручное обновление (v5 + `run.py`), затем живая проверка
|
||||
публикацией v6, сломанной v7 (откат) и v8.
|
||||
133
openspec/changes/agent-self-update/specs/agent/spec.md
Normal file
133
openspec/changes/agent-self-update/specs/agent/spec.md
Normal file
@ -0,0 +1,133 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Состав и настройки агента
|
||||
Агент SHALL состоять из файла `agent.py` (Python ≥ 3.9, только стандартная библиотека), запускателя `run.py` (те же
|
||||
ограничения) и файла `agent.json`: `hub_url`, `hub_token`, `jira.base_url`, `jira.auth` (`basic` с `user`/`password`
|
||||
или `bearer` с `token`), `verify_ssl`, `ca_bundle`, `timeout`, `poll_seconds`, необязательный `auto_update` (по
|
||||
умолчанию `true`). `hub_url` вида `http://…` или `https://…` SHALL означать HTTP-транспорт, вида
|
||||
`folder:<абсолютный путь>` — транспорт через общую папку. Всё остальное (id полей, типы, карта переходов, типы связей,
|
||||
доска, темп) агент SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT
|
||||
отправляться в хаб и MUST NOT попадать в журнал, логи и общую папку. Команды `agent.py`: `run` (по умолчанию), `check`,
|
||||
`selftest`, `prune`.
|
||||
|
||||
#### Scenario: Проверка без записи
|
||||
- **WHEN** `python3 agent.py check`
|
||||
- **THEN** агент печатает доступность хаба (версия настроек) и Jira (`GET /myself`), в Jira ничего не пишет
|
||||
|
||||
#### Scenario: Проверка через папку
|
||||
- **WHEN** `hub_url = "folder:/Volumes/My Shared Files/RSHB/jira-bridge"` и реле хаба работает
|
||||
- **THEN** `check` печатает «хаб: доступен через папку /Volumes/My Shared Files/RSHB/jira-bridge, настройки v<N>, проект
|
||||
<KEY>»; если каталога нет или реле не ответило за 10 с — строка с «НЕДОСТУПЕН» и причиной, код выхода 1
|
||||
|
||||
#### Scenario: Самопроверка новой версии
|
||||
- **WHEN** `python3 agent.py selftest`, хаб и Jira доступны
|
||||
- **THEN** печатается «jira-agent v<N>» и строки `check`; код 0; блокировка журнала не берётся, в Jira ничего не пишется;
|
||||
если хаб или Jira недоступны — код 3
|
||||
|
||||
### Requirement: Доставка и запуск
|
||||
README агента SHALL описывать: первую установку (копирование `agent.py`, `run.py` и `agent.example.json` через общую
|
||||
папку в локальный каталог виртуалки, заполнение `agent.json` с выбором транспорта в `hub_url`, `check`), запуск
|
||||
`python3 run.py` в терминале и через `nohup` с логом, опциональный launchd-plist с `run.py`, самообновление и
|
||||
выключатель `auto_update`, ручное обновление `run.py`, `prune` (только при остановленном агенте, иначе откажет).
|
||||
Версия агента SHALL быть в константе `AGENT_VERSION` файла (5 — самообновление) и в каждом `claim` и отчёте; хаб
|
||||
предупреждает в `agent_status`, если версия ниже минимальной из настроек.
|
||||
|
||||
#### Scenario: Устаревший агент
|
||||
- **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2`
|
||||
- **THEN** `agent_status.warning` = «обновите агента», команды выдаются по-прежнему
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Самообновление агента
|
||||
Агент SHALL передавать в каждом `claim` `update_state` (содержимое `update.json` рядом с `agent.py` или `null`) и
|
||||
`launcher_version` (из переменной окружения `JIRA_AGENT_LAUNCHER` или `null`). Проверять обновление агент SHALL только
|
||||
после ответа `claim` с `command = null`, без сигнала остановки, при `auto_update ≠ false` и
|
||||
`agent_release.version > AGENT_VERSION`. Версию с теми же `to_version` и `sha256`, что в `update.json` в состоянии
|
||||
`rejected` или `rolled_back`, агент MUST NOT ставить снова. Без `JIRA_AGENT_LAUNCHER` агент MUST NOT выходить для
|
||||
обновления: он пишет в лог «доступна v<N>; для самообновления запускайте через run.py» не чаще раза в час.
|
||||
|
||||
Подготовка версии SHALL идти по шагам: `GET /agent/release`; совпадение `version`/`sha256` ответа с `agent_release`
|
||||
(иначе временный сбой); sha256 от `content` в UTF-8 равен `sha256` (иначе `rejected`); `content` компилируется и
|
||||
содержит `AGENT_VERSION = <version>` (иначе `rejected`); запись `agent.py.new` атомарно; `python3 agent.py.new selftest`
|
||||
с таймаутом 120 с — код 0 годен, код 3 или таймаут — временный сбой, иной код — `rejected` с последними 500 символами
|
||||
вывода. Годную версию агент SHALL отметить в `update.json` состоянием `staged` и завершить `run` с кодом 75. При
|
||||
`rejected` и временном сбое `agent.py.new` SHALL удаляться, агент продолжает работу; при `rejected` пишется
|
||||
`update.json`; повтор после временного сбоя — не раньше чем через 5 минут. Первый успешный `claim` версии, для которой
|
||||
`update.json` = `trial` с `to_version = AGENT_VERSION`, SHALL записывать состояние `ok`. `update.json` пишется атомарно
|
||||
(временный файл и переименование).
|
||||
|
||||
#### Scenario: Обновление в паузе
|
||||
- **WHEN** агент v5 под запускателем получил `claim` без команды и `agent_release = {version: 6, sha256: S}`, файл
|
||||
проходит проверки и `selftest`
|
||||
- **THEN** рядом лежит `agent.py.new`, `update.json` = `{state: staged, from_version: 5, to_version: 6, sha256: S}`,
|
||||
процесс завершается с кодом 75; журнал не изменился
|
||||
|
||||
#### Scenario: Команда в очереди
|
||||
- **WHEN** опубликована v6, а `claim` агента v5 вернул команду
|
||||
- **THEN** агент выполняет команду и отчитывается; к обновлению переходит только после `claim` без команды
|
||||
|
||||
#### Scenario: Неверный sha256
|
||||
- **WHEN** `GET /agent/release` вернул `content`, sha256 которого не равен `sha256`
|
||||
- **THEN** `agent.py.new` не остаётся, `update.json` = `rejected` «sha256 не сходится»; следующий `claim` несёт этот
|
||||
`update_state`; агент v5 работает дальше и v6 с тем же sha256 больше не скачивает
|
||||
|
||||
#### Scenario: Хаб недоступен во время selftest
|
||||
- **WHEN** `selftest` новой версии вернул код 3
|
||||
- **THEN** `agent.py.new` удалён, `update.json` не изменён, следующая попытка — не раньше чем через 5 минут
|
||||
|
||||
#### Scenario: Запуск без запускателя
|
||||
- **WHEN** агент v5 запущен `python3 agent.py`, опубликована v6
|
||||
- **THEN** агент не скачивает файл и не выходит; в логе раз в час «доступна v6; для самообновления запускайте через
|
||||
run.py»; `claim` несёт `launcher_version = null`
|
||||
|
||||
#### Scenario: Подтверждение новой версии
|
||||
- **WHEN** запускатель запустил v6, `update.json` = `trial` с `to_version = 6`, первый `claim` v6 успешен
|
||||
- **THEN** `update.json` = `ok`; лог «обновление до v6 подтверждено»
|
||||
|
||||
### Requirement: Запускатель run.py
|
||||
`python3 run.py [аргументы]` SHALL работать с файлами в каталоге самого `run.py` (`agent.py`, `agent.py.new`,
|
||||
`agent.py.prev`, `update.json`) и запускать дочерним процессом `agent.py run` с переданными аргументами, переменной
|
||||
окружения `JIRA_AGENT_LAUNCHER = LAUNCHER_VERSION` и унаследованными stdout/stderr. Запускатель SHALL брать
|
||||
неблокирующую блокировку `run.py.lock`; занята — «запускатель уже работает», код 1. SIGINT запускатель SHALL
|
||||
игнорировать, SIGTERM — пересылать дочернему процессу.
|
||||
|
||||
По коду выхода дочернего процесса:
|
||||
- `0` или `130` — запускатель завершается с тем же кодом;
|
||||
- `75` — при `update.json` = `staged` и совпадении sha256 `agent.py.new`: копия `agent.py` в `agent.py.prev`, атомарная
|
||||
замена `agent.py` на `agent.py.new`, `update.json` = `trial`, запуск новой версии; иначе `agent.py.new` удаляется,
|
||||
`update.json` = `rejected` «файл изменился после проверки», запуск текущей версии;
|
||||
- иной код при `update.json` = `trial` — откат; иначе — перезапуск той же версии с паузой 5 с, удваиваемой до 300 с
|
||||
при каждом следующем падении и сбрасываемой после 10 минут работы.
|
||||
|
||||
Пока `update.json` = `trial`, запускатель SHALL ждать состояния `ok` не дольше 10 минут; по истечении — SIGTERM, через
|
||||
60 с SIGKILL и откат. Откат: `agent.py.prev` возвращается на место `agent.py`, `update.json` = `rolled_back` с причиной
|
||||
(«код <N> через <S> с» или «не вышла на связь за 10 мин»), прежняя версия запускается без паузы. Файл `agent.py` MUST
|
||||
существовать на каждом шаге подмены. При старте запускатель SHALL выполнять подмену, если `update.json` = `staged` и `agent.py.new` есть, и удалять
|
||||
`agent.py.new` без состояния `staged`.
|
||||
|
||||
#### Scenario: Подмена версии
|
||||
- **WHEN** дочерний v5 вышел с кодом 75, `update.json` = `staged` для v6, sha256 `agent.py.new` совпадает
|
||||
- **THEN** `agent.py.prev` — прежний v5, `agent.py` — v6, `update.json` = `trial`, запущен v6
|
||||
|
||||
#### Scenario: Откат при падении на старте
|
||||
- **WHEN** `update.json` = `trial` для v7, дочерний процесс вышел с кодом 1 через 3 с
|
||||
- **THEN** `agent.py` — снова v6 (из `agent.py.prev`), `update.json` = `{state: rolled_back, to_version: 7, error: "код 1
|
||||
через 3 с"}`, запущен v6; v6 видит `rolled_back` для v7 с тем же sha256 и v7 больше не ставит
|
||||
|
||||
#### Scenario: Новая версия не выходит на связь
|
||||
- **WHEN** `update.json` = `trial`, дочерний процесс работает, но за 10 минут `ok` не записан
|
||||
- **THEN** запускатель посылает SIGTERM, через 60 с при необходимости SIGKILL, делает откат с причиной «не вышла на связь
|
||||
за 10 мин»
|
||||
|
||||
#### Scenario: Падение после подтверждения
|
||||
- **WHEN** `update.json` = `ok`, агент дважды подряд упал с кодом 1, проработав меньше минуты
|
||||
- **THEN** запускатель перезапускает ту же версию через 5 с, затем через 10 с; отката нет
|
||||
|
||||
#### Scenario: Остановка в терминале
|
||||
- **WHEN** `python3 run.py` работает в терминале, нажат Ctrl-C
|
||||
- **THEN** агент доделывает команду и выходит с кодом 0, запускатель завершается с кодом 0; повторный Ctrl-C — агент
|
||||
выходит с 130, запускатель тоже
|
||||
|
||||
#### Scenario: Второй запускатель
|
||||
- **WHEN** `run.py` работает, и запускается ещё один `run.py` в том же каталоге
|
||||
- **THEN** второй пишет «запускатель уже работает» и завершается с кодом 1, агента не запускает
|
||||
89
openspec/changes/agent-self-update/specs/hub-api/spec.md
Normal file
89
openspec/changes/agent-self-update/specs/hub-api/spec.md
Normal file
@ -0,0 +1,89 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Маршруты агента
|
||||
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
|
||||
- `POST /agent/claim` `{agent_version, settings_version, last_jira_call_at, update_state, launcher_version}` — долгий
|
||||
запрос до 30 с; ответ `{command: {id, type, params, pause_seconds}, settings_version, agent_release}` или `{command:
|
||||
null, settings_version, agent_release}`, где `agent_release` — `{version, sha256}` опубликованной версии агента или
|
||||
`null`; обновляет `agent_status` (`last_jira_call_at = null` не затирает прежнее значение). `update_state` (объект или
|
||||
`null`, `error` хранится до 500 символов) и `launcher_version` (целое или `null`) необязательны; хаб SHALL хранить
|
||||
последние значения в памяти процесса, значения неверного типа игнорировать, не отвергая `claim`. Если есть команда в
|
||||
`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` или `failed` без результата (результат записывается, статус не меняется, заметка `new:N`
|
||||
переносится на ключ). Отчёт по команде в `queued` SHALL отвергаться 409 `not_claimed`. `error` хаб SHALL обрезать до
|
||||
1500 символов.
|
||||
- `POST /agent/state` `{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}` — загрузка состояния
|
||||
пачками; `neighbor_keys` — ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало и `sync_run`. Каждая
|
||||
пачка применяется независимо; `page` и `last_page` необязательны и хабом не используются.
|
||||
- `GET /agent/known-keys` — ключи зеркала (для определения соседей).
|
||||
- `GET /agent/release` — опубликованная версия агента `{version, sha256, content}` (`content` — текст `agent.py`);
|
||||
нет публикации или файл не сходится с манифестом по sha256 — 404 `no_release`.
|
||||
|
||||
#### Scenario: Выдача с подстановкой ключа
|
||||
- **WHEN** следующая команда ссылается на `new:60`, а `ensure_issue` для `new:60` завершилась с `AURORA-1590`
|
||||
- **THEN** `claim` отдаёт команду с `AURORA-1590` в параметрах
|
||||
|
||||
#### Scenario: Повторный отчёт
|
||||
- **WHEN** агент дважды прислал `result` для команды 15
|
||||
- **THEN** второй ответ 200, статус и результат не изменились
|
||||
|
||||
#### Scenario: Отчёт по невыданной команде
|
||||
- **WHEN** `POST /agent/commands/16/result` с `status = done`, команда 16 в `queued`
|
||||
- **THEN** 409 `not_claimed`, команда осталась `queued`
|
||||
|
||||
#### Scenario: Длинная ошибка
|
||||
- **WHEN** агент прислал `failed` с `error` длиной 5000 символов
|
||||
- **THEN** в `command.error` первые 1500 символов
|
||||
|
||||
#### Scenario: Поздний отчёт после предела выдач
|
||||
- **WHEN** `ensure_issue {temp_key: new:8, …}` переведена хабом в `failed` «агент не отчитался после 3 выдач», затем агент
|
||||
прислал `done` с `result.key = AURORA-1630`
|
||||
- **THEN** команда `failed`, в `result` — `AURORA-1630`; заметка `new:8` перенесена на `AURORA-1630`
|
||||
|
||||
#### Scenario: Публикация в ответе claim
|
||||
- **WHEN** опубликована v6, агент v5 отправил `claim`, команд нет
|
||||
- **THEN** ответ `{command: null, settings_version, agent_release: {version: 6, sha256: …}}`
|
||||
|
||||
#### Scenario: Состояние обновления от агента
|
||||
- **WHEN** агент прислал в `claim` `update_state = {state: rolled_back, from_version: 6, to_version: 7, error: "код 1 через 3 с"}`
|
||||
и `launcher_version = 1`
|
||||
- **THEN** `GET /agent` отдаёт это `update_state`, `launcher_version = 1` и `update_hint` «откат с v7 на v6: код 1 через 3 с»
|
||||
|
||||
#### Scenario: Скачивание версии
|
||||
- **WHEN** агентским токеном `GET /agent/release`, опубликована v6
|
||||
- **THEN** 200 `{version: 6, sha256, content}`, sha256 от `content` в UTF-8 равен `sha256`; клиентским токеном — 401;
|
||||
без публикации — 404 `no_release`
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Публикация версии агента
|
||||
`jira-hub agent publish PATH` SHALL публиковать файл агента: текст в UTF-8 не больше 1 МБ, ровно одна строка
|
||||
`AGENT_VERSION = <целое>`, версия больше опубликованной. Файл SHALL записываться в `<JIRA_HUB_HOME>/release/agent.py`
|
||||
(каталог 0700), затем `manifest.json` `{version, sha256, size, published_at}`; оба — через временный файл и
|
||||
переименование, манифест последним. Команда SHALL работать без запущенного `serve` и печатать «опубликована v<N>,
|
||||
sha256 <первые 12 символов>…, <size> байт»; при ошибке — сообщение и код 1, опубликованное не меняется. Хаб SHALL
|
||||
читать манифест при каждом `claim`, `GET /agent/release` и `GET /agent`; битый манифест означает «публикации нет»
|
||||
(предупреждение в лог). `GET /agent` SHALL дополняться полями `release` (`{version, sha256, published_at}` или `null`),
|
||||
`update_state`, `launcher_version` и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована
|
||||
версия выше версии агента, агент онлайн, `launcher_version = null`), «откат с v<to> на v<from>: <error>»
|
||||
(`rolled_back`), «v<to> отвергнута: <error>» (`rejected`), иначе `null`.
|
||||
|
||||
#### Scenario: Публикация новой версии
|
||||
- **WHEN** опубликована v5, `jira-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6`
|
||||
- **THEN** в `release/` — новый `agent.py` и манифест с `version = 6`; вывод «опубликована v6, sha256 …»; код 0
|
||||
|
||||
#### Scenario: Версия не выше опубликованной
|
||||
- **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6`
|
||||
- **THEN** «опубликована v6, новая должна быть больше», код 1; `release/` не изменён
|
||||
|
||||
#### Scenario: Нет строки версии
|
||||
- **WHEN** публикуется файл без строки `AGENT_VERSION = <целое>` или с двумя такими строками
|
||||
- **THEN** сообщение об ошибке, код 1; `release/` не изменён
|
||||
|
||||
#### Scenario: Агент без запускателя
|
||||
- **WHEN** опубликована v6, агент v5 онлайн, в последнем `claim` `launcher_version = null`
|
||||
- **THEN** `GET /agent` → `update_hint` = «агент запущен без run.py — обновится только вручную»
|
||||
44
openspec/changes/agent-self-update/specs/mcp/spec.md
Normal file
44
openspec/changes/agent-self-update/specs/mcp/spec.md
Normal file
@ -0,0 +1,44 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### 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` | `id` | новый пакет |
|
||||
| `batch_cancel` | `id` | вид пакета; для команд пакета, уже взятых агентом, — строка «команда #N уже у агента и будет выполнена; остановить — `POST /commands/N/cancel` с `force: true`» |
|
||||
| `refresh` | `keys` или `full`, `wait` (bool) | без `wait` — строка с id поставленного пакета; с `wait` — итог пакета, как у `batch_wait` |
|
||||
| `agent_status` | — | онлайн/офлайн, версия, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек; публикация агента («публикация: v<N> от <дата>» или «публикации нет»), состояние обновления («обновление: <state> v<from> → v<to>»), запускатель («запускатель: v<N>» или «без запускателя»), `update_hint`, если он есть |
|
||||
| `sync_log` | `limit` | последние `sync_run` |
|
||||
| `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` |
|
||||
|
||||
Описание инструмента `batch_create` SHALL предупреждать, что `ensure_issue` должна нести `labels` с базовой меткой
|
||||
проекта (`AWPCA`): хаб и агент метки сами не добавляют, и задача без неё выпадает из базовой JQL и из зеркала.
|
||||
|
||||
Автор команд из 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 минут со статусами всех команд
|
||||
|
||||
#### Scenario: Статус агента с довыдачей
|
||||
- **WHEN** агент на связи, команда 40 выдана дважды и ещё без отчёта
|
||||
- **THEN** ответ `agent_status` содержит «агент онлайн» и строку о команде 40 с числом выдач 2
|
||||
|
||||
#### Scenario: Отмена пакета с командой у агента
|
||||
- **WHEN** `batch_cancel {id: 9}`, команда 30 пакета в `claimed`
|
||||
- **THEN** ответ содержит вид пакета и строку «команда #30 уже у агента и будет выполнена; остановить — …»
|
||||
|
||||
#### Scenario: Статус агента после отката
|
||||
- **WHEN** опубликована v7, агент v6 под запускателем v1 прислал `update_state = rolled_back` для v7
|
||||
- **THEN** ответ `agent_status` содержит «публикация: v7 от …», «обновление: rolled_back v6 → v7», «запускатель: v1» и
|
||||
«откат с v7 на v6: …»
|
||||
Loading…
x
Reference in New Issue
Block a user