From 719d4bceef2bb25f6b009f3d13920c3b00a87817 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 19 Oct 2026 20:21:25 +0400 Subject: [PATCH] =?UTF-8?q?chore(agent):=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8?= =?UTF-8?q?=D1=8F=2012,=20README=20=D0=B8=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B4=D0=BE=D1=81=D0=BE=D0=BA=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 2 +- CLAUDE.md | 2 +- README.md | 21 ++++++++++++------- .../agent-v12-project-boards/.openspec.yaml | 2 ++ .../agent-v12-project-boards/design.md | 19 +++++++++++++++++ src/agent.py | 2 +- tests/test_folder.py | 2 +- 7 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 openspec/changes/agent-v12-project-boards/.openspec.yaml create mode 100644 openspec/changes/agent-v12-project-boards/design.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 48c2a56..0bc527b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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` | | Папка | `FolderTransport` | транспорт `folder:` к хабу через общую папку: `req/.json` → реле хаба → `resp/.json`; ошибки как у `Http`; `HubClient` делегирует ему `call` (спека `jira-hub/openspec/changes/folder-transport/`) | `os`, `uuid` | | Журнал | `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` | | Цикл | `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` | diff --git a/CLAUDE.md b/CLAUDE.md index 4bfc0e6..f634c32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ ## Первая подсистема Исполнитель команд и цикл агента (спека `agent/spec.md`, `commands/spec.md`) — реализованы целиком: все типы команд, журнал, восстановление после перезапуска, 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`. ## Выкатка новой версии diff --git a/README.md b/README.md index b2c6808..9862c4c 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,10 @@ 3. `agent.json` содержит учётные данные Jira. Никуда не копировать и никуда не отправлять, кроме этой виртуалки. Хаб получает от агента только результаты команд и состояние задач, без паролей и токенов Jira. -Остальные настройки (id полей, типы задач, карта переходов, типы связей, доска, темп) агент получает от хаба при -старте и при каждом изменении `settings_version`; в `agent.json` их быть не должно. +Остальные настройки (id полей, типы задач, карта переходов, типы связей, темп) агент получает от хаба при +старте и при каждом изменении `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_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных | | `ensure_link` | создаёт связь; конфликт без `replace` — `failed`; с `replace` сначала удаляет лишнее | -| `ensure_sprint` | переносит в спринт доски (активные и будущие) или в бэклог | +| `ensure_sprint` | переносит в спринт доски `board_id` команды (без него — доски из настроек хаба; активные и будущие спринты) или в бэклог; доска без спринтов (канбан) — `failed` с текстом Jira | | `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 раз. Счётчики суммируются. После исчерпания — `failed` «результат неизвестен (обрыв связи с Jira), повтор безопасен». При остановке по сигналу команду подхватит восстановление. -- **Версия.** Константа `AGENT_VERSION` в `agent.py` (сейчас 11) отправляется в каждом `claim`, в каждом отчёте +- **Версия.** Константа `AGENT_VERSION` в `agent.py` (сейчас 12) отправляется в каждом `claim`, в каждом отчёте (`agent_version`) и в `/agent/state`. Версия 4 — сравнение текстов без различия `\r\n`/`\n`, полная пауза при времени журнала в будущем, `claim` без оценки из журнала, `prune` под блокировкой. Версия 5 — самообновление через `run.py`. Версия 9 — код 73 при занятом журнале, `OSError` при подготовке — временный сбой, проверка формата публикации, источник ошибки хаба в @@ -198,6 +200,9 @@ python3 agent.py prune Версия 10 — комментарии задач в состоянии `refresh`. Версия 11 — тип задачи из `issue_types` с ошибкой конфигурации (тип `bug` — «Ошибка»), команда `ensure_attachment`, устойчивое дочитывание комментариев (M-1, M-2 из #30). Агент 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`: «обновите агента»; команды при этом выдаются по-прежнему. @@ -234,8 +239,10 @@ python3 agent.py prune следующем `claim` — агент выполнит её через проверку. 2. **HTTPS хаба с корпоративным CA не поддержан.** `HubClient` работает без `ca_bundle` и `verify_ssl`: `hub_url` должен быть `http://`. Для Jira `verify_ssl` и `ca_bundle` работают. -3. **Спринт в `refresh` — только активные и будущие спринты доски.** Поле `sprint` берётся из Agile API по - этим спринтам (рулинг R-6). У задач закрытых спринтов `sprint = null`. +3. **Спринт в `refresh` — только активные и будущие спринты scrum-досок.** Поле `sprint` берётся из Agile API по + этим спринтам (рулинг R-6). У задач закрытых спринтов `sprint = null`. Задачи канбан-досок и досок неизвестного + типа — тоже `null`. Задача в спринтах двух досок получает имя спринта последней по списку `boards`. Если + scrum-доска не отдала спринты (удалена, стала канбаном), `refresh` завершается `failed` с текстом Jira (R-32). 4. **Особенности `refresh`.** В `result.issues` попадают только запрошенные задачи, соседи по связям — в `result.neighbors`. Если `jql` вернул пустую выдачу, состояние в хаб не отправляется (`pages = 0`). Отсутствующие ключи при `refresh` по `keys` попадают в `result.missing` (не больше 50). diff --git a/openspec/changes/agent-v12-project-boards/.openspec.yaml b/openspec/changes/agent-v12-project-boards/.openspec.yaml new file mode 100644 index 0000000..c1bc902 --- /dev/null +++ b/openspec/changes/agent-v12-project-boards/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-19 diff --git a/openspec/changes/agent-v12-project-boards/design.md b/openspec/changes/agent-v12-project-boards/design.md new file mode 100644 index 0000000..b575c52 --- /dev/null +++ b/openspec/changes/agent-v12-project-boards/design.md @@ -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` | diff --git a/src/agent.py b/src/agent.py index ac243e5..4e704bc 100644 --- a/src/agent.py +++ b/src/agent.py @@ -33,7 +33,7 @@ import uuid from datetime import datetime, timezone from typing import Any, Callable, Dict, List, Optional, Tuple -AGENT_VERSION = 11 +AGENT_VERSION = 12 EXIT_LOCKED = 73 # «журнал занят другим процессом»: для run.py это не сбой версии (design 8) EXIT_UPDATE = 75 # «обновление готово»: run.py подменит agent.py на agent.py.new HERE = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/test_folder.py b/tests/test_folder.py index 75b8221..f738461 100644 --- a/tests/test_folder.py +++ b/tests/test_folder.py @@ -183,7 +183,7 @@ class FolderIntegrationTest(unittest.TestCase): return jira_cfg(self.fake, hub_url="folder:" + self.root, hub_token=self.hub.token) def test_version(self): - self.assertEqual(agent.AGENT_VERSION, 11) + self.assertEqual(agent.AGENT_VERSION, 12) def test_runner_through_folder(self): self.fake.add("AURORA-1", duedate="2026-10-20")