47 lines
7.2 KiB
Markdown
47 lines
7.2 KiB
Markdown
# Архитектура
|
||
|
||
Агент — один файл `src/agent.py` и запускатель `src/run.py` (Python 3.9, только стандартная библиотека). Тесты в `tests/` работают на фейках
|
||
Jira и хаба. Спека: `../jira-hub/openspec/changes/bridge-core/`, план агента: `docs/superpowers/plans/2026-10-11-bridge-core-agent.md` (план хаба: `../jira-hub/docs/superpowers/plans/2026-10-11-bridge-core-hub.md`).
|
||
|
||
## Разделы файла `src/agent.py`
|
||
|
||
| Раздел | Классы и функции | Ответственность | Что использует |
|
||
|---|---|---|---|
|
||
| Настройки | `load_config` | читает `agent.json`, проверяет обязательные поля | — |
|
||
| 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` |
|
||
| Журнал | `Journal`, `_parse_at` | дозапись `journal.jsonl`: `started` → `finished` → `acked`; восстановление; `prune` (из CLI — под блокировкой журнала) | файловая система |
|
||
| Исполнитель | `Executor` и `do_<тип>` | по одному методу на тип команды: читает Jira, пишет только разницу; `refresh` читает и отправляет состояние в хаб, затем (v13, параметр `history`) журнал изменений Jira — `refresh_history`: изменившиеся с `history.since` задачи этого прогона и стартовая выборка `history.jql`, `search` с `expand=changelog`, `POST /agent/history` пачками по 500; ответ не 2xx Jira или хаба — `history.error`/`backfill_error`, `refresh` остаётся `done`; `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` |
|
||
|
||
## Запускатель `src/run.py`
|
||
|
||
Не импортирует `agent.py` (тот меняется). `Launcher` запускает `agent.py run` дочерним процессом с
|
||
`JIRA_AGENT_LAUNCHER`; коды 0/130 — выход; 75 — `apply_update` (копия в `agent.py.prev`, `os.replace`, `trial`);
|
||
падение в `trial` или 10 минут без `ok` — `rollback`; иначе перезапуск с паузой 5…300 с. Блокировка `run.py.lock`;
|
||
SIGINT при живом агенте игнорирует (Ctrl-C получает агент), SIGTERM пересылает. Спека — «Запускатель run.py».
|
||
|
||
`reconcile` при старте и `in_trial` перед каждым запуском сверяют `update.json` с sha256 `agent.py` (таблица — design 1 change'а
|
||
agent-v9-hardening); коды агента: 75 — подмена, 73 — журнал занят (не сбой версии); `OSError` при подмене — `rejected`,
|
||
запускатель не падает.
|
||
|
||
## Границы
|
||
- Код новой версии приходит только через API хаба (`GET /agent/release`) и пишется в каталог агента; общая папка для
|
||
кода не используется.
|
||
- Пароль и токен Jira — только в `agent.json`. В хаб, журнал и логи не попадают.
|
||
- Политика темпа не хранится в агенте: зазоры (`pacing`) и `pause_seconds` приходят от хаба. Агент досыпает паузу
|
||
до зазора от последнего вызова Jira (`JiraClient.last_call_at`).
|
||
- Одна команда в полёте: цикл берёт следующую только после выполнения и подтверждения отчёта.
|
||
- Отчёт до подтверждения: `send_result` повторяет каждые 10 с; 404/409/422 от хаба считаются закрытием.
|
||
- Журнал пишется трижды на команду вокруг выполнения: `started` — после паузы, перед первым обращением к Jira;
|
||
`finished` — после результата; `acked` — после подтверждения хаба. Восстановление опирается только на этот порядок.
|
||
- Каждый обработчик `Executor` возвращает `status`/`result`/`error` и счётчики. Исключение, которое не обработано
|
||
в обработчике, `Runner.execute` превращает в `failed` и не роняет цикл.
|
||
- Тесты не ходят во внешние сервисы; фейки — `tests/fake_jira.py` и `tests/fake_hub.py`.
|
||
|
||
## Открытые вопросы
|
||
- HTTPS хаба с корпоративным CA: `HubClient` не принимает `ca_bundle`/`verify_ssl` (ограничение README, пункт 2).
|
||
- Спринт задач закрытых спринтов: `sprint = null` (рулинг R-6).
|