docs(openspec): hub-hygiene-d — публикация, статус агента, CRLF, тесты (#32)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8229bbaa77
commit
0bb43431b8
2
openspec/changes/hub-hygiene-d/.openspec.yaml
Normal file
2
openspec/changes/hub-hygiene-d/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-10-15
|
||||||
105
openspec/changes/hub-hygiene-d/design.md
Normal file
105
openspec/changes/hub-hygiene-d/design.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Хаб `main` 8229bba (web-app и agent-v9-hardening слиты). Issue aep/jira-hub#32. Источники: #25 (M-1…M-3, M-5…M-7),
|
||||||
|
#16 (T3.1, T3.2, T5.2, M-8, ре-ревью minor-1, minor-3, O-9, O-10, O-11), #4 (M-9/O-4, M-10). Сверка с кодом — 15.10.2026.
|
||||||
|
|
||||||
|
Найдено при чтении кода:
|
||||||
|
- `release._write_atomic` — `tmp.write_bytes` + `os.replace`, без `fsync` и без очистки `.tmp` при ошибке; `publish`
|
||||||
|
пишет `agent.py`, затем манифест. `cli` ловит только `PublishError`. Если манифест не записался, новый `agent.py`
|
||||||
|
не сходится со старым манифестом по sha → `load` → `None` → 404.
|
||||||
|
- `release.current`: `int(m["version"])` (6.9 → 6, `true` → 1), `str(m["sha256"])`; `log.warning` на каждый вызов
|
||||||
|
(каждый `claim` и `GET /agent`).
|
||||||
|
- `release.load`: `data.decode("utf-8")` после сверки sha, `UnicodeDecodeError` не ловится → 500.
|
||||||
|
- `Hub.agent_update = {"update_state": None, "launcher_version": None}` — в памяти; после перезапуска хаба до первого
|
||||||
|
`claim` `launcher_version = None` неотличимо от «агент без запускателя» (`release.hint`, `mcp_server.fmt_agent:115`).
|
||||||
|
- `h_settings_put` отвечает `{"settings_version": version}`.
|
||||||
|
- `queue._next`: при неразрешённом временном ключе — `skipped` «задача new:N не создана», независимо от того,
|
||||||
|
`failed` или `cancelled` команда-создатель (`_definer_status`).
|
||||||
|
- `commands.check_confirmed`: `(issue.get(field) or None) != (p[field] or None)` — `\r\n` в зеркале против `\n` в
|
||||||
|
параметре даёт «не совпало».
|
||||||
|
- `relay._cleaned: set[Path]`, проверка `self.root not in self._cleaned` — по написанию пути.
|
||||||
|
- Тесты: 249 passed; 87 `ResourceWarning: unclosed database` (test_queue, test_relay, test_commands, test_importer).
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:** публикация не оставляет хаб без годной версии при сбое записи и не роняет команду трейсбеком; статус агента
|
||||||
|
не утверждает неизвестное; подтверждение по состоянию работает на задачах с CRLF; ответы и тексты называют то, что
|
||||||
|
произошло; тесты закрывают найденные пробелы и чисты от `ResourceWarning`.
|
||||||
|
|
||||||
|
**Non-Goals:** см. proposal.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Публикация агента
|
||||||
|
|
||||||
|
1. **Запись файла** (`_write_atomic`): временный файл `.<имя>.tmp` в том же каталоге, `write` + `flush` + `os.fsync`,
|
||||||
|
`os.replace`; при `OSError` временный файл удаляется (ошибки удаления игнорируются), исключение пробрасывается.
|
||||||
|
2. **Порядок публикации:** `agent.py` → `agent.py.prev` (`os.replace`, если `agent.py` есть); новый файл через
|
||||||
|
`_write_atomic` в `agent.py`; манифест через `_write_atomic`. `OSError` после переименования — `agent.py.prev`
|
||||||
|
возвращается на место `agent.py` (`os.replace`; если и это не удалось — в лог), и `PublishError("не удалось записать
|
||||||
|
публикацию: <ошибка>")`. После успешной записи манифеста `agent.py.prev` удаляется. Итог: при любом сбое опубликована
|
||||||
|
прежняя версия (манифест и `agent.py` согласованы), `release/` без временных файлов.
|
||||||
|
3. **`cli`** печатает текст `PublishError` в stderr и возвращает 1 — как сейчас; новых веток нет, `OSError` до `cli` не
|
||||||
|
доходит.
|
||||||
|
4. **Годный манифест** (`current`): объект; `version` — `int` и не `bool`, ≥ 1; `sha256` — строка, `re.fullmatch(r"[0-9a-f]{64}")`.
|
||||||
|
Иначе — «публикации нет». Совпадает с проверкой агента v9 (design agent-v9-hardening 10).
|
||||||
|
5. **Предупреждение о битом манифесте** — один раз на каждый новый текст ошибки: модульная переменная с последним
|
||||||
|
текстом; успешное чтение или отсутствие манифеста её сбрасывает. Отсутствие манифеста — не ошибка, без лога.
|
||||||
|
6. **`load`**: `UnicodeDecodeError` → `log.error` «agent.py не в UTF-8 — публикации нет» и `None` (404 `no_release`).
|
||||||
|
|
||||||
|
### Статус агента
|
||||||
|
|
||||||
|
7. **`agent_reported`**: `Hub.agent_update` получает ключ `reported: False`; `note_agent_update` (вызывается на каждом
|
||||||
|
`claim`) ставит `True`. `GET /agent` отдаёт `agent_reported`. `release.hint(...)` получает признак и не выдаёт
|
||||||
|
«агент запущен без run.py…», пока `agent_reported = false`; подсказки об откате и отказе — по `update_state` как
|
||||||
|
сейчас (до первого `claim` он `null`).
|
||||||
|
8. **MCP `agent_status`**: строка запускателя — «запускатель: v<N>», «без запускателя» (агент сообщил `null`) или
|
||||||
|
«запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)» (`agent_reported = false`).
|
||||||
|
Если поля `agent_reported` в ответе нет (старый хаб) — прежнее поведение.
|
||||||
|
|
||||||
|
### Настройки, очередь, подтверждение, реле
|
||||||
|
|
||||||
|
9. **`PUT /settings`** отвечает `settings.get_all(conn)` (токены скрыты) после записи — те же поля, что `GET /settings`,
|
||||||
|
включая `settings_version`. Клиенты, читающие `settings_version` из ответа (`client`, `mcp_server.settings_set`,
|
||||||
|
web), не меняются.
|
||||||
|
10. **Пропуск зависимой команды**: если среди неразрешённых ключей есть ключ, чья `ensure_issue` в `cancelled`, текст —
|
||||||
|
«создание задачи new:N отменено» (по первому такому ключу); иначе прежний «задача new:N не создана».
|
||||||
|
11. **Сравнение строк в `check_confirmed`**: для полей `ensure_fields` со строковыми значениями сравнение после замены
|
||||||
|
`\r\n` на `\n` с обеих сторон; `None` и пустая строка по-прежнему равны. Метки и прочие поля — без изменений.
|
||||||
|
12. **Реле**: `_cleaned` хранит `Path.resolve()`; проверка — по `self.root.resolve()`. Ошибка `resolve` (папки нет) —
|
||||||
|
сравнение по исходному пути, как сейчас.
|
||||||
|
|
||||||
|
### Тесты и чистота
|
||||||
|
|
||||||
|
13. Новые тесты: bool в строковом параметре и `[]` в списочном (`test_commands`); `sync_run.finished_at` не ставится
|
||||||
|
при `failed` не-refresh по пределу выдач (`test_queue`); 501 на `DELETE`/`PATCH` (`test_api`); `epics.csv` без
|
||||||
|
«Ключ» (`test_importer`); «подготовлена заново» в `test_vanished_tree_keeps_work` (`test_relay`); предупреждение
|
||||||
|
«обновите агента» при `agent_version < min_agent_version`, офлайн-агент (нет контакта дольше порога) в
|
||||||
|
`GET /agent`, `tick` — проверка эффекта (`test_api`/`test_queue`); публикация — сбой записи манифеста возвращает
|
||||||
|
прежний `agent.py`, `.tmp` нет; битый манифест (`6.9`, `true`, короткий sha) — «публикации нет», предупреждение один
|
||||||
|
раз; не-UTF-8 → 404; `agent_reported` до и после `claim`; `PUT /settings` отдаёт `relay_dir`; текст «отменено»;
|
||||||
|
CRLF-подтверждение; реле с симлинком на ту же папку.
|
||||||
|
14. **`ResourceWarning`**: фикстуры и хелперы тестов закрывают соединения (`conn.close()` / `contextlib.closing`);
|
||||||
|
критерий — `uv run pytest -W error::ResourceWarning` без предупреждений `unclosed database` (pytest
|
||||||
|
`filterwarnings = error::ResourceWarning` в `pyproject.toml`, если не ломает сторонние пакеты; иначе — проверка в
|
||||||
|
плане командой).
|
||||||
|
15. **T3.1**: `_type_error` переписывается без тяжёлой строки с `and` и кортежем — отдельные ветки по типу.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [`agent.py.prev` в `release/`] лишний файл живёт только между переименованием и записью манифеста; при падении
|
||||||
|
процесса посреди — остаётся; следующая публикация его перезапишет. → не влияет на `load` (читается только `agent.py`).
|
||||||
|
- [«неизвестно» до первого claim] до 30 с после перезапуска хаба статус неполный. → честнее ложного «без запускателя».
|
||||||
|
- [`PUT /settings` отдаёт больше полей] клиенты, ожидавшие ровно `{settings_version}`, получают надмножество. → поле на
|
||||||
|
месте.
|
||||||
|
- [Конфликт с #29] текстовый в `tests/test_api.py` (одна строка у #29). → кто сливается вторым, разрешает.
|
||||||
|
|
||||||
|
## Decisions made without discussion
|
||||||
|
|
||||||
|
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| R-1 (design #7) | 2026-10-15 | Признак `agent_reported` в памяти, а не `launcher_version` в БД | без записи на каждый claim; значение из БД после перезапуска могло устареть | до 30 с «неизвестно» после перезапуска хаба |
|
||||||
|
| R-2 (design #2) | 2026-10-15 | Откат публикации через `agent.py.prev` (переименования, без копии) | переименование не требует места на диске — откат работает и при полном диске | лишний файл в `release/` при падении процесса посреди публикации |
|
||||||
|
| R-3 (design #9) | 2026-10-15 | `PUT /settings` отвечает всеми настройками, а не `{settings_version, relay_dir}` | один формат с `GET /settings`; O-11 — «не видно, что записалось» относится к любой настройке | надмножество полей в ответе |
|
||||||
|
| R-4 (design #10) | 2026-10-15 | «Отменено» — только при `cancelled` создателе; при `failed` текст прежний | O-10 описывает только force-отмену; `failed` — задача действительно не создана | нет |
|
||||||
|
| R-5 (design #14) | 2026-10-15 | Цель — ноль `ResourceWarning` от sqlite в тестах, проверка `-W error::ResourceWarning` | M-9/O-4: 87 предупреждений прячут настоящие утечки | время на правку фикстур |
|
||||||
57
openspec/changes/hub-hygiene-d/proposal.md
Normal file
57
openspec/changes/hub-hygiene-d/proposal.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
У хаба накопились отложенные находки трёх change'ей: самообновления (#25), пакета C (#16) и первого change'а (#4,
|
||||||
|
«на потом»). Разбор против `main` 8229bba (15.10.2026) показал, что актуальны все пункты, кроме M-4 #25, который исправило
|
||||||
|
слияние web-app. Вред у одного пользователя небольшой, но заметный:
|
||||||
|
- при полном диске `jira-hub agent publish` падает с трейсбеком и оставляет публикацию в состоянии «404 до следующей
|
||||||
|
публикации»;
|
||||||
|
- после перезапуска хаба `agent_status` пишет «без запускателя», хотя агент под `run.py`;
|
||||||
|
- потерянный отчёт `ensure_fields` по задаче, у которой описание с `\r\n` (71 из 180 правлены в UI), закрывается
|
||||||
|
довыдачей вместо подтверждения по состоянию;
|
||||||
|
- в тестах 87 незакрытых соединений sqlite и пробелы в покрытии.
|
||||||
|
|
||||||
|
Решения владельца 15.10.2026: всё актуальное — одним пакетом; M-4 #25 закрыть, minor-2 #16 отклонить; агентский
|
||||||
|
#26 в пакет не входит; работать параллельно с #29 (web-app follow-ups), она трогает только `h_app`.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Публикация агента** (`release.py`, `cli.py`): запись с `fsync`; прежний `agent.py` переименовывается в
|
||||||
|
`agent.py.prev`, при сбое записи манифеста возвращается; `OSError` → «не удалось записать публикацию: <ошибка>»,
|
||||||
|
код 1, временные файлы удалены; манифест годен, только если `version` — целое (не bool) и `sha256` — 64 hex-символа;
|
||||||
|
не-UTF-8 файл при совпавшем sha — 404, а не 500; предупреждение о битом манифесте — один раз на каждую новую ошибку.
|
||||||
|
- **`GET /agent`**: поле `agent_reported` (был ли `claim` с момента запуска хаба); пока его нет — подсказки про `run.py`
|
||||||
|
нет; MCP `agent_status` пишет «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)».
|
||||||
|
- **`PUT /settings`**: ответ — все настройки, как `GET /settings` (включая `settings_version`).
|
||||||
|
- **Очередь**: зависимая команда, пропущенная из-за force-отменённой `ensure_issue`, получает
|
||||||
|
«создание задачи new:N отменено».
|
||||||
|
- **Подтверждение по состоянию**: строки `ensure_fields` сравниваются без различия `\r\n` и `\n`.
|
||||||
|
- **Реле**: обработанные папки сравниваются по `resolve()`.
|
||||||
|
- **Тесты**: bool в строковом параметре и пустой список; `finished_at` у `sync_run` только для `refresh`; 501 на прочие
|
||||||
|
методы; `epics.csv` без ключа; «подготовлена заново» в реле; предупреждение `min_agent_version`; офлайн-агент; `tick` с
|
||||||
|
проверками; ноль `ResourceWarning`; читаемый `commands._type_error`.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
<!-- нет -->
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `hub-api`: ответ `PUT /settings`; публикация версии агента (сбой записи, проверка манифеста, `agent_reported`).
|
||||||
|
- `mcp`: строка запускателя в `agent_status`.
|
||||||
|
- `commands`: текст `skipped` при отменённом создании; подтверждение `ensure_fields` без различия перевода строки.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- сохранение `launcher_version` в БД (запись на каждый claim; «неизвестно» честнее устаревшего значения);
|
||||||
|
- агент (aep/jira-agent#26) — в следующий change агента;
|
||||||
|
- minor-2 #16 (дубль предупреждения при неудачном `chmod`) — отклонено;
|
||||||
|
- раздача веб-приложения (`h_app`) — в #29.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- jira-hub (issue aep/jira-hub#32): `src/jira_hub/release.py`, `cli.py`, `api.py` (`Hub`, `h_agent_get`,
|
||||||
|
`h_settings_put`), `queue.py` (`_next`), `commands.py` (`check_confirmed`, `_type_error`), `relay.py` (`_cleaned`),
|
||||||
|
`mcp_server.py` (`fmt_agent`), `tests/`, ARCHITECTURE.md при необходимости.
|
||||||
|
- Пересечение с `feat/29-web-app-followups`: `api.py` (другие функции) и одна строка `tests/test_api.py`; дельты спек
|
||||||
|
трогают разные требования (`hub-api` «Раздача веб-приложения» у #29).
|
||||||
|
- Выкатка: после merge — pull на Mac mini, перезапуск хаба (владелец), `/mcp` → reconnect в сессиях Claude.
|
||||||
62
openspec/changes/hub-hygiene-d/specs/commands/spec.md
Normal file
62
openspec/changes/hub-hygiene-d/specs/commands/spec.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Временные ключи и ссылки внутри пакета
|
||||||
|
Параметр-ключ любой команды MAY быть временным `new:<n>`. Хаб SHALL выдавать агенту такую команду только после `done`
|
||||||
|
команды `ensure_issue` с тем же `temp_key` в том же пакете, подставив настоящий ключ. Если `ensure_issue` завершилась
|
||||||
|
`failed` или `cancelled`, зависимые команды SHALL получить `skipped` с ошибкой «создание задачи new:<n> отменено», если
|
||||||
|
`ensure_issue` отменена, иначе «задача new:<n> не создана». Временный ключ вне пакета с его `ensure_issue` SHALL
|
||||||
|
отвергаться при создании пакета (422).
|
||||||
|
|
||||||
|
#### Scenario: Создать и привязать
|
||||||
|
- **WHEN** пакет: `ensure_issue {temp_key: new:60, …}`, `ensure_link {source: new:60, target: AURORA-1537, …}`
|
||||||
|
- **THEN** вторая команда выдаётся агенту с `source = AURORA-1590` после завершения первой
|
||||||
|
|
||||||
|
#### Scenario: Создание отменено
|
||||||
|
- **WHEN** пакет: `ensure_issue {temp_key: new:901, …}` (отменена с `force: true`), `ensure_comment {key: new:901, …}`
|
||||||
|
- **THEN** `ensure_comment` — `skipped` с ошибкой «создание задачи new:901 отменено»
|
||||||
|
|
||||||
|
#### Scenario: Создание упало
|
||||||
|
- **WHEN** `ensure_issue {temp_key: new:902, …}` завершилась `failed`, зависимая `ensure_link {source: new:902, …}`
|
||||||
|
- **THEN** `ensure_link` — `skipped` с ошибкой «задача new:902 не создана»
|
||||||
|
|
||||||
|
### Requirement: Ошибки и подтверждение по состоянию
|
||||||
|
Ошибка команды SHALL записываться в `error` (HTTP-код, текст Jira); хаб SHALL хранить не больше 1500 символов `error`,
|
||||||
|
обрезая присланное. Пакет после ошибки продолжается.
|
||||||
|
|
||||||
|
**Повторная выдача.** Агент запрашивает новую команду, только когда у него нет незавершённой (см. `agent`), поэтому
|
||||||
|
команда в `claimed` к моменту следующего `claim` считается не доставленной или потерянной агентом. При `claim` хаб SHALL
|
||||||
|
сначала выдать снова самую старую (по `claimed_at`, затем `id`) команду в `claimed`: тот же `id` и параметры,
|
||||||
|
`pause_seconds` по текущей политике темпа, новый `claimed_at`, `deliveries + 1`. Если у неё уже `deliveries >= 3`, хаб SHALL
|
||||||
|
перевести её в `failed` с `error = "агент не отчитался после 3 выдач"` и перейти к следующей (довыдача другой `claimed`
|
||||||
|
или следующая `queued`). Довыдача — доставка той же выдачи, а не повтор команды: новых команд хаб MUST NOT создавать
|
||||||
|
сам, повтор после `failed` — новая команда с теми же параметрами по запросу интерфейса. Если агент всё же отчитался
|
||||||
|
`done` с ключом по `ensure_issue`, переведённой в `failed` по пределу, хаб SHALL записать ключ в результат и перенести
|
||||||
|
заметку, не меняя статус (как для отменённой, см. `hub-api`).
|
||||||
|
|
||||||
|
**Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб
|
||||||
|
SHALL перевести её в `confirmed_by_state`: `ensure_fields` — все поля совпадают (строки сравниваются без различия
|
||||||
|
`\r\n` и `\n`; `null` и пустая строка равны); `ensure_status` — статус равен последнему в пути; `ensure_link` — связь
|
||||||
|
есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment` и `refresh` по состоянию не
|
||||||
|
подтверждаются: потерянный отчёт по ним закрывается повторной выдачей (агент находит созданную задачу поиском дубля)
|
||||||
|
или отменой человеком.
|
||||||
|
|
||||||
|
#### Scenario: Отчёт потерян, факт на месте
|
||||||
|
- **WHEN** `ensure_sprint {key: X, sprint: "Спринт 2"}` в `claimed`, после выполненного `refresh` зеркало показывает X в «Спринт 2»
|
||||||
|
- **THEN** команда `confirmed_by_state`
|
||||||
|
|
||||||
|
#### Scenario: Описание с CRLF в зеркале
|
||||||
|
- **WHEN** `ensure_fields {key: X, description: "a\nb"}` в `claimed`, после `refresh` в зеркале у X `description = "a\r\nb"`
|
||||||
|
- **THEN** команда `confirmed_by_state`
|
||||||
|
|
||||||
|
#### Scenario: Ответ на claim потерян
|
||||||
|
- **WHEN** команда 40 выдана (`deliveries = 1`), ответ до агента не дошёл, агент снова прислал `claim`
|
||||||
|
- **THEN** `claim` отдаёт команду 40, `deliveries = 2`; в очереди есть `queued` команды, но выдаётся 40
|
||||||
|
|
||||||
|
#### Scenario: Предел выдач
|
||||||
|
- **WHEN** команда 41 в `claimed` с `deliveries = 3`, агент снова прислал `claim`, в очереди есть команда 42
|
||||||
|
- **THEN** 41 — `failed` «агент не отчитался после 3 выдач»; `claim` отдаёт 42
|
||||||
|
|
||||||
|
#### Scenario: Потерянный отчёт о создании задачи
|
||||||
|
- **WHEN** `ensure_issue {temp_key: new:5, summary: "Отчёт"}` выполнена агентом, отчёт не дошёл, агент прислал `claim`
|
||||||
|
- **THEN** команда выдаётся снова; агент находит созданную задачу поиском дубля и отчитывается `done` с её ключом; второй
|
||||||
|
задачи нет, заметка `new:5` перенесена на ключ
|
||||||
99
openspec/changes/hub-hygiene-d/specs/hub-api/spec.md
Normal file
99
openspec/changes/hub-hygiene-d/specs/hub-api/spec.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
## 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<N>, sha256 <первые 12 символов>…, <size> байт»; при ошибке — сообщение и код 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<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`, `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`
|
||||||
49
openspec/changes/hub-hygiene-d/specs/mcp/spec.md
Normal file
49
openspec/changes/hub-hygiene-d/specs/mcp/spec.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
## 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>», «без запускателя» или, пока `agent_reported = false`, «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)»), `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: …»
|
||||||
|
|
||||||
|
#### Scenario: Статус агента сразу после перезапуска хаба
|
||||||
|
- **WHEN** хаб перезапущен, агент ещё не прислал `claim`
|
||||||
|
- **THEN** ответ `agent_status` содержит «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска
|
||||||
|
хаба)» и не содержит «без запускателя» и «агент запущен без run.py»
|
||||||
Loading…
x
Reference in New Issue
Block a user