## 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, проект »; если каталога нет или реле не ответило за 10 с — строка с «НЕДОСТУПЕН» и причиной, код выхода 1 #### Scenario: Самопроверка новой версии - **WHEN** `python3 agent.py selftest`, хаб и Jira доступны - **THEN** печатается «jira-agent v» и строки `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; для самообновления запускайте через run.py» не чаще раза в час. Подготовка версии SHALL идти по шагам: `GET /agent/release`; совпадение `version`/`sha256` ответа с `agent_release` (иначе временный сбой); sha256 от `content` в UTF-8 равен `sha256` (иначе `rejected`); `content` компилируется и содержит `AGENT_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` с причиной («код через с» или «не вышла на связь за 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, агента не запускает