Andrey Epifancev 0bb43431b8 docs(openspec): hub-hygiene-d — публикация, статус агента, CRLF, тесты (#32)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:33:07 +04:00

9.9 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Маршруты интерфейсов

Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном:

  • GET /issues с фильтрами sprint, assignee, work_kind, fact_status, status, label, epic, neighbor, q (подстрока темы), limit/offset — строки зеркала (все поля issue) с fact_status и work_kind из заметок; сокращение до карточки — дело интерфейса.
  • GET /issues/{key} — задача целиком: поля, связи в обе стороны, заметки, последние 10 команд по ней.
  • PUT /issues/{key}/note — заметки (частичное обновление, updated_by из тела).
  • POST /batches {title, author, commands: [...]} → {id, estimate_seconds}; 422 при неизвестном типе, неверных параметрах (в том числе неверного типа значения), временном ключе без ensure_issue, двух и более ensure_issue с одинаковой summary (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы).
  • GET /batches?limit=, GET /batches/{id} (с командами), POST /batches/{id}/cancel — отменяет queued команды пакета; ответ — вид пакета с полем still_claimed: список {id, type} команд пакета в claimed (они будут выполнены агентом; остановить — отмена команды с force).
  • POST /commands/{id}/retry — новая команда с теми же параметрами в новом пакете «Повтор команды N».
  • POST /commands/{id}/cancel — только для queued; для claimed — 409, если не передан force: true.
  • POST /refresh {keys | jql | full: true} → пакет из одного refresh.
  • GET /agent, GET /sync-runs?limit=, GET /settings, PUT /settings (частично; поднимает settings_version; ответ — все настройки после записи, как у GET /settings, включая settings_version).

Scenario: Фильтр по спринту

  • WHEN GET /issues?sprint=Спринт 2
  • THEN только задачи с таким sprint, отсортированы по сроку, затем по ключу

Scenario: Отмена взятой команды

  • WHEN POST /commands/15/cancel без force, команда claimed
  • THEN 409 с claimed_at; с force: true — cancelled, следующий отчёт агента по ней принимается, статус не меняется

Scenario: Поздний отчёт по отменённому созданию

  • WHEN ensure_issue {temp_key: new:7, …} отменена с force: true, затем агент прислал done с result.key = AURORA-1620
  • THEN команда cancelled, в result — AURORA-1620; заметка new:7 перенесена на AURORA-1620

Scenario: Дубль темы в пакете

  • WHEN POST /batches с двумя ensure_issue, у обеих summary = "Шахматка: комментарии"
  • THEN 422 с темой «Шахматка: комментарии», пакет не создан

Scenario: Тема не строка

  • WHEN POST /batches с ensure_issue {temp_key: new:1, summary: 42, …}
  • THEN 422 с указанием параметра summary, пакет не создан

Scenario: Отмена пакета с командой у агента

  • WHEN в пакете 9 команда 30 claimed, команды 31 и 32 queued; POST /batches/9/cancel
  • THEN 31 и 32 cancelled; в ответе still_claimed = [{id: 30, type: …}]; 30 остаётся claimed

Scenario: Ответ на запись настроек

  • WHEN PUT /settings {relay_dir: "/Volumes/externalSSD/utm/rshb/jira-bridge"}
  • THEN 200; ответ содержит relay_dir с записанным значением, новую settings_version и остальные настройки; токенов в ответе нет

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}; оба — через временный файл с fsync и переименование, манифест последним. Прежний agent.py на время публикации SHALL переименовываться в agent.py.prev и удаляться после записи манифеста; если запись нового файла или манифеста не удалась (OSError), хаб SHALL вернуть agent.py.prev на место agent.py, удалить временные файлы и завершить команду сообщением «не удалось записать публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя версия. Команда SHALL работать без запущенного serve и печатать «опубликована v, sha256 <первые 12 символов>…, байт»; при ошибке — сообщение и код 1, опубликованное не меняется. Хаб SHALL читать манифест при каждом claim, GET /agent/release и GET /agent; манифест годен, только если это объект, version — целое число (не логическое) не меньше 1, sha256 — строка из 64 символов [0-9a-f]; негодный или нечитаемый манифест означает «публикации нет» и предупреждение в лог — одно на каждый новый текст ошибки. Файл agent.py, не декодируемый как UTF-8 при совпавшем sha256, означает «публикации нет» (GET /agent/release — 404 no_release). GET /agent SHALL дополняться полями release ({version, sha256, published_at} или null), update_state, launcher_version, agent_reported (был ли claim с момента запуска хаба) и update_hint: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии агента, агент онлайн, agent_reported = true, launcher_version = null), «откат с v на v: » (rolled_back), «v отвергнута: » (rejected), иначе null.

Scenario: Публикация новой версии

  • WHEN опубликована v5, jira-hub agent publish jira-agent/src/agent.py с AGENT_VERSION = 6
  • THEN в release/ — новый agent.py и манифест с version = 6, agent.py.prev нет; вывод «опубликована v6, sha256 …»; код 0

Scenario: Версия не выше опубликованной

  • WHEN опубликована v6, публикуется файл с AGENT_VERSION = 6
  • THEN «опубликована v6, новая должна быть больше», код 1; release/ не изменён

Scenario: Нет строки версии

  • WHEN публикуется файл без строки AGENT_VERSION = <целое> или с двумя такими строками
  • THEN сообщение об ошибке, код 1; release/ не изменён

Scenario: Диск полон при записи манифеста

  • WHEN опубликована v8, публикуется v9, запись manifest.json завершилась OSError
  • THEN «не удалось записать публикацию: …», код 1, без трейсбека; agent.py и манифест — прежние v8, GET /agent/release отдаёт v8; временных файлов и agent.py.prev в release/ нет

Scenario: Негодный манифест

  • WHEN в manifest.json version = 6.9 (или true, или sha256 короче 64 символов), хаб получает 100 claim
  • THEN в claim agent_release = null, GET /agent/release — 404; в логе одно предупреждение о манифесте

Scenario: Агент без запускателя

  • WHEN опубликована v6, агент v5 онлайн, в последнем claim launcher_version = null
  • THEN GET /agent → agent_reported = true, update_hint = «агент запущен без run.py — обновится только вручную»

Scenario: Хаб перезапущен, агент ещё не выходил на связь

  • WHEN опубликована v10, агент v9 под запускателем, хаб перезапущен, claim после запуска ещё не было
  • THEN GET /agent → agent_reported = false, launcher_version = null, update_hint = null; после первого claim — agent_reported = true, launcher_version из claim