chore(agent): версия 12, README и решения агента для досок проектов (#35)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-19 20:21:25 +04:00
parent 9dd3dd7d9d
commit 719d4bceef
7 changed files with 39 additions and 11 deletions

View File

@ -11,7 +11,7 @@ Jira и хаба. Спека: `../jira-hub/openspec/changes/bridge-core/`, пл
| HTTP | `Http`, `HttpError`, `JiraClient`, `HubClient`, `multipart_file`, `wire_filename` | запросы; `Http.send` — запрос с готовым телом (JSON-вызов `call` идёт через него); `JiraClient` считает вызовы, повторяет 429/503 по `Retry-After` (до 5 запросов, не более 4 ожиданий), хранит время последнего вызова; `JiraClient.upload` — вложение `multipart/form-data` (таймаут не меньше 120 с); `HubClient.file` — файл вложения у хаба (base64 в JSON) | `urllib` | | HTTP | `Http`, `HttpError`, `JiraClient`, `HubClient`, `multipart_file`, `wire_filename` | запросы; `Http.send` — запрос с готовым телом (JSON-вызов `call` идёт через него); `JiraClient` считает вызовы, повторяет 429/503 по `Retry-After` (до 5 запросов, не более 4 ожиданий), хранит время последнего вызова; `JiraClient.upload` — вложение `multipart/form-data` (таймаут не меньше 120 с); `HubClient.file` — файл вложения у хаба (base64 в JSON) | `urllib` |
| Папка | `FolderTransport` | транспорт `folder:` к хабу через общую папку: `req/<id>.json` → реле хаба → `resp/<id>.json`; ошибки как у `Http`; `HubClient` делегирует ему `call` (спека `jira-hub/openspec/changes/folder-transport/`) | `os`, `uuid` | | Папка | `FolderTransport` | транспорт `folder:` к хабу через общую папку: `req/<id>.json` → реле хаба → `resp/<id>.json`; ошибки как у `Http`; `HubClient` делегирует ему `call` (спека `jira-hub/openspec/changes/folder-transport/`) | `os`, `uuid` |
| Журнал | `Journal`, `_parse_at` | дозапись `journal.jsonl`: `started` → `finished` → `acked`; восстановление; `prune` (из CLI — под блокировкой журнала) | файловая система | | Журнал | `Journal`, `_parse_at` | дозапись `journal.jsonl`: `started` → `finished` → `acked`; восстановление; `prune` (из CLI — под блокировкой журнала) | файловая система |
| Исполнитель | `Executor` и `do_<тип>` | по одному методу на тип команды: читает Jira, пишет только разницу; `refresh` читает и отправляет состояние в хаб; `ensure_attachment` берёт файл вложения у хаба (`hub_file`: sha256 и размер, повреждение — `HubError` с повтором) | `JiraClient`, `HubClient` (`refresh` и файл в `ensure_attachment`), настройки хаба | | Исполнитель | `Executor` и `do_<тип>` | по одному методу на тип команды: читает Jira, пишет только разницу; `refresh` читает и отправляет состояние в хаб; `ensure_attachment` берёт файл вложения у хаба (`hub_file`: sha256 и размер, повреждение — `HubError` с повтором); доски — из параметров команд (`ensure_sprint.board_id`, `refresh.boards`, тип доски — `board_type`), `board_id` настроек — запасной | `JiraClient`, `HubClient` (`refresh` и файл в `ensure_attachment`), настройки хаба |
| Самообновление | `read_update_state`, `write_update_state`, `Updater`, `launcher_version` | `update.json`; после `claim` без команды — проверка `agent_release`, скачивание, sha256, компиляция, `selftest`, `staged`; подтверждение `ok` новой версией; без запускателя — только подсказка в лог | `HubClient`, `subprocess` | | Самообновление | `read_update_state`, `write_update_state`, `Updater`, `launcher_version` | `update.json`; после `claim` без команды — проверка `agent_release`, скачивание, sha256, компиляция, `selftest`, `staged`; подтверждение `ok` новой версией; без запускателя — только подсказка в лог | `HubClient`, `subprocess` |
| Цикл | `Runner` | claim, пауза до команды, журнал, выполнение, отчёт до подтверждения, восстановление при старте; после `claim` без команды спрашивает `Updater`, готова версия — выход с `EXIT_UPDATE` (75) | `HubClient`, `JiraClient`, `Journal`, `Executor` | | Цикл | `Runner` | claim, пауза до команды, журнал, выполнение, отчёт до подтверждения, восстановление при старте; после `claim` без команды спрашивает `Updater`, готова версия — выход с `EXIT_UPDATE` (75) | `HubClient`, `JiraClient`, `Journal`, `Executor` |
| CLI | `cmd_run`, `cmd_check`, `cmd_selftest`, `cmd_prune`, `main` | команды `run`, `check`, `selftest`, `prune` (`run` и `prune` берут `acquire_lock` на `<журнал>.lock`); обработка сигналов | `Runner`, `Journal` | | CLI | `cmd_run`, `cmd_check`, `cmd_selftest`, `cmd_prune`, `main` | команды `run`, `check`, `selftest`, `prune` (`run` и `prune` берут `acquire_lock` на `<журнал>.lock`); обработка сигналов | `Runner`, `Journal` |

View File

@ -48,7 +48,7 @@
## Первая подсистема ## Первая подсистема
Исполнитель команд и цикл агента (спека `agent/spec.md`, `commands/spec.md`) — реализованы целиком: все типы команд, Исполнитель команд и цикл агента (спека `agent/spec.md`, `commands/spec.md`) — реализованы целиком: все типы команд,
журнал, восстановление после перезапуска, CLI `run|check|selftest|prune`, самообновление через `run.py`. журнал, восстановление после перезапуска, CLI `run|check|selftest|prune`, самообновление через `run.py`.
Последний change — `../jira-hub/openspec/changes/bug-and-attachments/` (агент v11: тип «Ошибка», `ensure_attachment`, устойчивое дочитывание комментариев; решения агента — `openspec/changes/agent-v11-bug-attachments/design.md`), план — `docs/superpowers/plans/2026-10-15-bug-and-attachments-agent.md`. Последний change — `../jira-hub/openspec/changes/projects/` (агент v12: доски проектов из параметров команд, тип доски в `refresh`; решения агента — `openspec/changes/agent-v12-project-boards/design.md`), план — `docs/superpowers/plans/2026-10-19-projects-agent.md`.
План и спека живут в `jira-hub`: `openspec/changes/bridge-core/` и `docs/superpowers/plans/2026-10-11-bridge-core-hub.md`; план агента лежит здесь: `docs/superpowers/plans/2026-10-11-bridge-core-agent.md`. План и спека живут в `jira-hub`: `openspec/changes/bridge-core/` и `docs/superpowers/plans/2026-10-11-bridge-core-hub.md`; план агента лежит здесь: `docs/superpowers/plans/2026-10-11-bridge-core-agent.md`.
## Выкатка новой версии ## Выкатка новой версии

View File

@ -24,8 +24,10 @@
3. `agent.json` содержит учётные данные Jira. Никуда не копировать и никуда не отправлять, кроме этой виртуалки. 3. `agent.json` содержит учётные данные Jira. Никуда не копировать и никуда не отправлять, кроме этой виртуалки.
Хаб получает от агента только результаты команд и состояние задач, без паролей и токенов Jira. Хаб получает от агента только результаты команд и состояние задач, без паролей и токенов Jira.
Остальные настройки (id полей, типы задач, карта переходов, типы связей, доска, темп) агент получает от хаба при Остальные настройки (id полей, типы задач, карта переходов, типы связей, темп) агент получает от хаба при
старте и при каждом изменении `settings_version`; в `agent.json` их быть не должно. старте и при каждом изменении `settings_version`; в `agent.json` их быть не должно. Доски Jira (с v12) приходят в
параметрах команд: `ensure_sprint` — `board_id`, `refresh` — `boards`; `board_id` из настроек — запасной для команд
старого хаба.
## Проверка ## Проверка
@ -170,9 +172,9 @@ python3 agent.py prune
| `ensure_comment` | комментарий с маркером в начале: обновляет, если текст изменился, иначе создаёт | | `ensure_comment` | комментарий с маркером в начале: обновляет, если текст изменился, иначе создаёт |
| `ensure_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных | | `ensure_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных |
| `ensure_link` | создаёт связь; конфликт без `replace` — `failed`; с `replace` сначала удаляет лишнее | | `ensure_link` | создаёт связь; конфликт без `replace` — `failed`; с `replace` сначала удаляет лишнее |
| `ensure_sprint` | переносит в спринт доски (активные и будущие) или в бэклог | | `ensure_sprint` | переносит в спринт доски `board_id` команды (без него — доски из настроек хаба; активные и будущие спринты) или в бэклог; доска без спринтов (канбан) — `failed` с текстом Jira |
| `ensure_attachment` | ищет у задачи вложение с тем же именем и размером — есть: ничего не делает; нет: берёт файл у хаба (`/agent/files/{sha256}`), сверяет sha256 и размер и загружает в Jira (`multipart/form-data`); файл удалён с хаба — `failed` «загрузите заново»; повреждённый файл — повтор, как обрыв связи с хабом. Содержимое файла в журнал и лог не пишется | | `ensure_attachment` | ищет у задачи вложение с тем же именем и размером — есть: ничего не делает; нет: берёт файл у хаба (`/agent/files/{sha256}`), сверяет sha256 и размер и загружает в Jira (`multipart/form-data`); файл удалён с хаба — `failed` «загрузите заново»; повреждённый файл — повтор, как обрыв связи с хабом. Содержимое файла в журнал и лог не пишется |
| `refresh` | только читает Jira и отправляет состояние задач в хаб; ничего не пишет | | `refresh` | только читает Jira и отправляет состояние задач в хаб; ничего не пишет; доски с неизвестным типом (`boards`, `type = null`) — запрос `/rest/agile/1.0/board/{id}`, тип в результате `boards`; спринты — со всех scrum-досок |
Правила работы: Правила работы:
- **Проверь, потом сделай.** Каждый тип сначала читает текущее состояние и записывает только разницу. - **Проверь, потом сделай.** Каждый тип сначала читает текущее состояние и записывает только разницу.
@ -190,7 +192,7 @@ python3 agent.py prune
`finished`, ждёт не меньше 30 с и повторяет команду через проверку («проверь, потом сделай») до 3 раз. Счётчики `finished`, ждёт не меньше 30 с и повторяет команду через проверку («проверь, потом сделай») до 3 раз. Счётчики
суммируются. После исчерпания — `failed` «результат неизвестен (обрыв связи с Jira), повтор безопасен». При остановке суммируются. После исчерпания — `failed` «результат неизвестен (обрыв связи с Jira), повтор безопасен». При остановке
по сигналу команду подхватит восстановление. по сигналу команду подхватит восстановление.
- **Версия.** Константа `AGENT_VERSION` в `agent.py` (сейчас 11) отправляется в каждом `claim`, в каждом отчёте - **Версия.** Константа `AGENT_VERSION` в `agent.py` (сейчас 12) отправляется в каждом `claim`, в каждом отчёте
(`agent_version`) и в `/agent/state`. Версия 4 — сравнение текстов без различия `\r\n`/`\n`, полная пауза при времени (`agent_version`) и в `/agent/state`. Версия 4 — сравнение текстов без различия `\r\n`/`\n`, полная пауза при времени
журнала в будущем, `claim` без оценки из журнала, `prune` под блокировкой. Версия 5 — самообновление через `run.py`. журнала в будущем, `claim` без оценки из журнала, `prune` под блокировкой. Версия 5 — самообновление через `run.py`.
Версия 9 — код 73 при занятом журнале, `OSError` при подготовке — временный сбой, проверка формата публикации, источник ошибки хаба в Версия 9 — код 73 при занятом журнале, `OSError` при подготовке — временный сбой, проверка формата публикации, источник ошибки хаба в
@ -198,6 +200,9 @@ python3 agent.py prune
Версия 10 — комментарии задач в состоянии `refresh`. Версия 11 — тип задачи из `issue_types` с ошибкой конфигурации Версия 10 — комментарии задач в состоянии `refresh`. Версия 11 — тип задачи из `issue_types` с ошибкой конфигурации
(тип `bug` — «Ошибка»), команда `ensure_attachment`, устойчивое дочитывание комментариев (M-1, M-2 из #30). (тип `bug` — «Ошибка»), команда `ensure_attachment`, устойчивое дочитывание комментариев (M-1, M-2 из #30).
Агент v10 на `ensure_attachment` отвечает `failed` «неизвестный тип команды»; хаб после выкатки ставит `min_agent_version = 11`. Агент v10 на `ensure_attachment` отвечает `failed` «неизвестный тип команды»; хаб после выкатки ставит `min_agent_version = 11`.
Версия 12 — доски проектов из параметров команд (`ensure_sprint.board_id`, `refresh.boards`), тип доски в результате
`refresh`, спринты со всех scrum-досок; без этих параметров — `board_id` из настроек, как v11. Хаб с проектами
выкатывается только после v12 (design хаба `projects`, решение 14).
Если хаб назначил `min_agent_version` выше, он пишет в `agent_status.warning`: «обновите агента»; команды Если хаб назначил `min_agent_version` выше, он пишет в `agent_status.warning`: «обновите агента»; команды
при этом выдаются по-прежнему. при этом выдаются по-прежнему.
@ -234,8 +239,10 @@ python3 agent.py prune
следующем `claim` — агент выполнит её через проверку. следующем `claim` — агент выполнит её через проверку.
2. **HTTPS хаба с корпоративным CA не поддержан.** `HubClient` работает без `ca_bundle` и `verify_ssl`: `hub_url` 2. **HTTPS хаба с корпоративным CA не поддержан.** `HubClient` работает без `ca_bundle` и `verify_ssl`: `hub_url`
должен быть `http://`. Для Jira `verify_ssl` и `ca_bundle` работают. должен быть `http://`. Для Jira `verify_ssl` и `ca_bundle` работают.
3. **Спринт в `refresh` — только активные и будущие спринты доски.** Поле `sprint` берётся из Agile API по 3. **Спринт в `refresh` — только активные и будущие спринты scrum-досок.** Поле `sprint` берётся из Agile API по
этим спринтам (рулинг R-6). У задач закрытых спринтов `sprint = null`. этим спринтам (рулинг R-6). У задач закрытых спринтов `sprint = null`. Задачи канбан-досок и досок неизвестного
типа — тоже `null`. Задача в спринтах двух досок получает имя спринта последней по списку `boards`. Если
scrum-доска не отдала спринты (удалена, стала канбаном), `refresh` завершается `failed` с текстом Jira (R-32).
4. **Особенности `refresh`.** В `result.issues` попадают только запрошенные задачи, соседи по связям — в 4. **Особенности `refresh`.** В `result.issues` попадают только запрошенные задачи, соседи по связям — в
`result.neighbors`. Если `jql` вернул пустую выдачу, состояние в хаб не отправляется (`pages = 0`). `result.neighbors`. Если `jql` вернул пустую выдачу, состояние в хаб не отправляется (`pages = 0`).
Отсутствующие ключи при `refresh` по `keys` попадают в `result.missing` (не больше 50). Отсутствующие ключи при `refresh` по `keys` попадают в `result.missing` (не больше 50).

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-19

View File

@ -0,0 +1,19 @@
# agent-v12-project-boards — решения агента
Спека и дизайн изменения — в `jira-hub/openspec/changes/projects/` (issue aep/jira-hub#61, aep/jira-agent#35).
Агентская часть — решения 7, 14, 15 дизайна хаба и требования `agent` «Доски проектов» (ADDED) и «Состав и настройки
агента» (MODIFIED); контракт команд — `commands` (`ensure_sprint.board_id`, `refresh.boards`, результат `boards`).
## Decisions made without discussion
Решения 1–21 записаны в `jira-hub/openspec/changes/projects/design.md` (раздел «Decisions made without discussion»;
для агента — № 6, 7, 14); здесь — только решения агентской части.
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| R-30 | 2026-10-19 | Типы досок определяются первым шагом `refresh` — до поиска и в том числе при пустых `keys` | Спека: «для каждой доски с `type = null` агент SHALL запросить» — без условий; хвост пакета `refresh {keys: []}` не должен оставлять тип неизвестным | Лишний `GET /board/{id}` на доску неизвестного типа, если поиск затем упадёт; результат с типом при этом теряется до следующего `refresh` |
| R-31 | 2026-10-19 | Любой `HttpError` на `GET /board/{id}` (в том числе 5xx) — тип `null` и строка в логе, `refresh` продолжается; `ConnectionError` не ловится — весь `refresh` `failed` с повтором, как при обрыве связи с Jira | Спека: «ошибка запроса — в лог, `refresh` продолжается»; `null` тип в хабе не меняет, следующий `refresh` спросит снова; обрыв связи — не свойство доски | Временный 5xx откладывает определение типа до следующего `refresh` (автообновление) |
| R-32 | 2026-10-19 | Ошибка чтения спринтов scrum-доски (известной или только что определённой) в `refresh` не глотается: `refresh` `failed` с текстом Jira, как v11 | Пропуск доски записал бы в зеркало `sprint = null` всем её задачам; спека о такой ошибке молчит | Одна сломанная доска (удалена, стала канбаном) останавливает обновление зеркала всех проектов, пока владелец не поправит доску в базе хаба |
| R-33 | 2026-10-19 | Запасной `settings.board_id` — только когда ключа `boards` нет (`refresh`) или `board_id` нет / `null` (`ensure_sprint`); `boards: []` — ни одной доски, спринты не читаются | Новый хаб оставляет `board_id` в настройках (решение 14 хаба) и шлёт `boards` всегда; `[]` от него значит «у проектов нет досок», а не «старый хаб» | Если старый хаб когда-нибудь пришлёт `boards: []`, спринты пропадут из зеркала; такого хаба нет |
| R-34 | 2026-10-19 | Ключ `boards` в результате `refresh` — только если агент запросил тип хотя бы одной доски; иначе ключа нет | Спека: результат «для досок, чей тип агент запрашивал»; сценарий «Старый хаб» — «результат без `boards`» | Хаб SHALL принимать результат без `boards`; если он ждёт `{}`, — ошибка разбора результата в хабе |
| R-35 | 2026-10-19 | Доски сравниваются и выводятся по `str(id)`; повторы в `boards` отбрасываются (запрос типа — один); элемент без `id` пропускается; тип принимается только точным `"scrum"`/`"kanban"`, иное (`"simple"` и т. п.) — `null` со строкой в логе; спринты читаются в порядке `boards`, последнее найденное имя побеждает | Ключи результата в спеке — строки; хаб шлёт без повторов, но повтор не должен стоить второго запроса; сценарий спеки — «один раз» | Доска `"4216"` строкой и `4216` числом считаются одной; другой регистр типа от Jira (`"Scrum"`) дал бы `null` |

View File

@ -33,7 +33,7 @@ import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
AGENT_VERSION = 11 AGENT_VERSION = 12
EXIT_LOCKED = 73 # «журнал занят другим процессом»: для run.py это не сбой версии (design 8) EXIT_LOCKED = 73 # «журнал занят другим процессом»: для run.py это не сбой версии (design 8)
EXIT_UPDATE = 75 # «обновление готово»: run.py подменит agent.py на agent.py.new EXIT_UPDATE = 75 # «обновление готово»: run.py подменит agent.py на agent.py.new
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))

View File

@ -183,7 +183,7 @@ class FolderIntegrationTest(unittest.TestCase):
return jira_cfg(self.fake, hub_url="folder:" + self.root, hub_token=self.hub.token) return jira_cfg(self.fake, hub_url="folder:" + self.root, hub_token=self.hub.token)
def test_version(self): def test_version(self):
self.assertEqual(agent.AGENT_VERSION, 11) self.assertEqual(agent.AGENT_VERSION, 12)
def test_runner_through_folder(self): def test_runner_through_folder(self):
self.fake.add("AURORA-1", duedate="2026-10-20") self.fake.add("AURORA-1", duedate="2026-10-20")