From 6a9f79374844204e58d66957f89e9e5a584a185e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 15 Oct 2026 11:04:39 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20hub-hygiene-d=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../2026-10-15-hub-hygiene-d}/.openspec.yaml | 0 .../2026-10-15-hub-hygiene-d}/design.md | 0 .../2026-10-15-hub-hygiene-d}/proposal.md | 0 .../specs/commands/spec.md | 0 .../specs/hub-api/spec.md | 0 .../specs/mcp/spec.md | 0 openspec/specs/commands/spec.md | 26 +++++++--- openspec/specs/hub-api/spec.md | 50 +++++++++++++++---- openspec/specs/mcp/spec.md | 7 ++- 9 files changed, 65 insertions(+), 18 deletions(-) rename openspec/changes/{hub-hygiene-d => archive/2026-10-15-hub-hygiene-d}/.openspec.yaml (100%) rename openspec/changes/{hub-hygiene-d => archive/2026-10-15-hub-hygiene-d}/design.md (100%) rename openspec/changes/{hub-hygiene-d => archive/2026-10-15-hub-hygiene-d}/proposal.md (100%) rename openspec/changes/{hub-hygiene-d => archive/2026-10-15-hub-hygiene-d}/specs/commands/spec.md (100%) rename openspec/changes/{hub-hygiene-d => archive/2026-10-15-hub-hygiene-d}/specs/hub-api/spec.md (100%) rename openspec/changes/{hub-hygiene-d => archive/2026-10-15-hub-hygiene-d}/specs/mcp/spec.md (100%) diff --git a/openspec/changes/hub-hygiene-d/.openspec.yaml b/openspec/changes/archive/2026-10-15-hub-hygiene-d/.openspec.yaml similarity index 100% rename from openspec/changes/hub-hygiene-d/.openspec.yaml rename to openspec/changes/archive/2026-10-15-hub-hygiene-d/.openspec.yaml diff --git a/openspec/changes/hub-hygiene-d/design.md b/openspec/changes/archive/2026-10-15-hub-hygiene-d/design.md similarity index 100% rename from openspec/changes/hub-hygiene-d/design.md rename to openspec/changes/archive/2026-10-15-hub-hygiene-d/design.md diff --git a/openspec/changes/hub-hygiene-d/proposal.md b/openspec/changes/archive/2026-10-15-hub-hygiene-d/proposal.md similarity index 100% rename from openspec/changes/hub-hygiene-d/proposal.md rename to openspec/changes/archive/2026-10-15-hub-hygiene-d/proposal.md diff --git a/openspec/changes/hub-hygiene-d/specs/commands/spec.md b/openspec/changes/archive/2026-10-15-hub-hygiene-d/specs/commands/spec.md similarity index 100% rename from openspec/changes/hub-hygiene-d/specs/commands/spec.md rename to openspec/changes/archive/2026-10-15-hub-hygiene-d/specs/commands/spec.md diff --git a/openspec/changes/hub-hygiene-d/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-15-hub-hygiene-d/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/hub-hygiene-d/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-15-hub-hygiene-d/specs/hub-api/spec.md diff --git a/openspec/changes/hub-hygiene-d/specs/mcp/spec.md b/openspec/changes/archive/2026-10-15-hub-hygiene-d/specs/mcp/spec.md similarity index 100% rename from openspec/changes/hub-hygiene-d/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-15-hub-hygiene-d/specs/mcp/spec.md diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md index 9c34f26..dd2fc2a 100644 --- a/openspec/specs/commands/spec.md +++ b/openspec/specs/commands/spec.md @@ -55,13 +55,22 @@ ### Requirement: Временные ключи и ссылки внутри пакета Параметр-ключ любой команды MAY быть временным `new:`. Хаб SHALL выдавать агенту такую команду только после `done` команды `ensure_issue` с тем же `temp_key` в том же пакете, подставив настоящий ключ. Если `ensure_issue` завершилась -`failed` или `cancelled`, зависимые команды SHALL получить `skipped`. Временный ключ вне пакета с его `ensure_issue` -SHALL отвергаться при создании пакета (422). +`failed` или `cancelled`, зависимые команды SHALL получить `skipped` с ошибкой «создание задачи new: отменено», если +`ensure_issue` отменена, иначе «задача new: не создана». Временный ключ вне пакета с его `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 создаваться одним вызовом с автором, заголовком и списком команд; хаб SHALL назначить `seq` по порядку списка и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh`). Хаб SHALL проверять @@ -111,15 +120,20 @@ SHALL выдерживать зазор типа команды между за заметку, не меняя статус (как для отменённой, см. `hub-api`). **Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб -SHALL перевести её в `confirmed_by_state`: `ensure_fields` — все поля совпадают; `ensure_status` — статус равен последнему -в пути; `ensure_link` — связь есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment` -и `refresh` по состоянию не подтверждаются: потерянный отчёт по ним закрывается повторной выдачей (агент находит -созданную задачу поиском дубля) или отменой человеком. +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 diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index 863b17b..d8fbbe7 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -100,7 +100,8 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - `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 /agent`, `GET /sync-runs?limit=`, `GET /settings`, `PUT /settings` (частично; поднимает `settings_version`; + ответ — все настройки после записи, как у `GET /settings`, включая `settings_version`). #### Scenario: Фильтр по спринту - **WHEN** `GET /issues?sprint=Спринт 2` @@ -126,6 +127,11 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача - **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, хаб SHALL скопировать файл базы в `backups/<дата-время>-.sqlite` и хранить последние `backup_keep` копий. @@ -237,18 +243,26 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача ### Requirement: Публикация версии агента `jira-hub agent publish PATH` SHALL публиковать файл агента: текст в UTF-8 не больше 1 МБ, ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной. Файл SHALL записываться в `/release/agent.py` -(каталог 0700), затем `manifest.json` `{version, sha256, size, published_at}`; оба — через временный файл и -переименование, манифест последним. Команда SHALL работать без запущенного `serve` и печатать «опубликована v, -sha256 <первые 12 символов>…, байт»; при ошибке — сообщение и код 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 на v: » -(`rolled_back`), «v отвергнута: » (`rejected`), иначе `null`. +(каталог 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`; вывод «опубликована v6, sha256 …»; код 0 +- **THEN** в `release/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6, + sha256 …»; код 0 #### Scenario: Версия не выше опубликованной - **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6` @@ -258,9 +272,23 @@ sha256 <первые 12 символов>…, байт»; при ошиб - **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` → `update_hint` = «агент запущен без run.py — обновится только вручную» +- **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` ### Requirement: Раздача веб-приложения Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/jira_hub/web/app/`, читая файлы diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md index ce47256..7f45847 100644 --- a/openspec/specs/mcp/spec.md +++ b/openspec/specs/mcp/spec.md @@ -27,7 +27,7 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм | `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 от <дата>» или «публикации нет»), состояние обновления («обновление: v → v»), запускатель («запускатель: v» или «без запускателя»), `update_hint`, если он есть | +| `agent_status` | — | онлайн/офлайн, версия, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек; публикация агента («публикация: v от <дата>» или «публикации нет»), состояние обновления («обновление: v → v»), запускатель («запускатель: v», «без запускателя» или, пока `agent_reported = false`, «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)»), `update_hint`, если он есть | | `sync_log` | `limit` | последние `sync_run` | | `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` | @@ -56,3 +56,8 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм - **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»