Merge pull request 'docs(openspec): архив folder-transport (#5)' (#7) from chore/5-archive-folder-transport into main
Reviewed-on: aep/jira-hub#7
This commit is contained in:
commit
799b0e245e
@ -1,7 +1,7 @@
|
|||||||
# jira-hub
|
# jira-hub
|
||||||
|
|
||||||
## Что это
|
## Что это
|
||||||
Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; текущий change — `openspec/changes/folder-transport/`.
|
Хаб моста Jira для АРМ КА: единственный источник истины по задачам проекта AURORA (метка AWPCA) на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; текущего change нет (последний — `openspec/changes/archive/2026-10-13-folder-transport/`).
|
||||||
|
|
||||||
## Стек и ограничения
|
## Стек и ограничения
|
||||||
- Python 3.13, uv, hatchling, pytest; HTTP на stdlib (`http.server`), БД — SQLite (stdlib), MCP — пакет `mcp` (закреплён `<2`, рулинг R-10).
|
- Python 3.13, uv, hatchling, pytest; HTTP на stdlib (`http.server`), БД — SQLite (stdlib), MCP — пакет `mcp` (закреплён `<2`, рулинг R-10).
|
||||||
|
|||||||
@ -81,3 +81,4 @@ Non-goals — см. proposal.md.
|
|||||||
| 2026-10-12 | `relay_dir` не входит в настройки для агента (`AGENT_KEYS`) | агенту путь на стороне Mac mini не нужен | нет |
|
| 2026-10-12 | `relay_dir` не входит в настройки для агента (`AGENT_KEYS`) | агенту путь на стороне Mac mini не нужен | нет |
|
||||||
| 2026-10-13 | В `ARCHITECTURE.md` граница «HTTP живёт только в `api` и `client`» дополнена `relay` (R-1) | реле само ходит в HTTP своего хаба; иначе документ противоречит коду | нет |
|
| 2026-10-13 | В `ARCHITECTURE.md` граница «HTTP живёт только в `api` и `client`» дополнена `relay` (R-1) | реле само ходит в HTTP своего хаба; иначе документ противоречит коду | нет |
|
||||||
| 2026-10-13 | В `tests/test_py39.py` агента в белый список stdlib добавлен `uuid` (R-2, подробно — `jira-agent/openspec/changes/folder-transport/design.md`) | спека требует `uuid4().hex` | нет |
|
| 2026-10-13 | В `tests/test_py39.py` агента в белый список stdlib добавлен `uuid` (R-2, подробно — `jira-agent/openspec/changes/folder-transport/design.md`) | спека требует `uuid4().hex` | нет |
|
||||||
|
| 2026-10-13 | Пункты, по которым финальный ревьюер воздержался от оценки (несколько агентов, права файлов с токеном, M-2 bridge-core, лавина потоков, симлинки, IPv6, частота опроса, ожидание полного timeout без реле, `hub_url` не строкой, двойные строки лога), не исправлялись (R-3) | все вне спеки или приняты владельцем в этом design.md | нет: поведение соответствует спеке |
|
||||||
@ -8,14 +8,20 @@ TBD - created by archiving change bridge-core. Update Purpose after archive.
|
|||||||
### Requirement: Состав и настройки агента
|
### Requirement: Состав и настройки агента
|
||||||
Агент SHALL состоять из одного файла `agent.py` (Python ≥ 3.9, только стандартная библиотека) и файла `agent.json`:
|
Агент SHALL состоять из одного файла `agent.py` (Python ≥ 3.9, только стандартная библиотека) и файла `agent.json`:
|
||||||
`hub_url`, `hub_token`, `jira.base_url`, `jira.auth` (`basic` с `user`/`password` или `bearer` с `token`), `verify_ssl`,
|
`hub_url`, `hub_token`, `jira.base_url`, `jira.auth` (`basic` с `user`/`password` или `bearer` с `token`), `verify_ssl`,
|
||||||
`ca_bundle`, `timeout`, `poll_seconds`. Всё остальное (id полей, типы, карта переходов, типы связей, доска, темп) агент
|
`ca_bundle`, `timeout`, `poll_seconds`. `hub_url` вида `http://…` или `https://…` SHALL означать HTTP-транспорт, вида
|
||||||
SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT отправляться в хаб
|
`folder:<абсолютный путь>` — транспорт через общую папку. Всё остальное (id полей, типы, карта переходов, типы связей,
|
||||||
и MUST NOT попадать в журнал и логи.
|
доска, темп) агент SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT
|
||||||
|
отправляться в хаб и MUST NOT попадать в журнал, логи и общую папку.
|
||||||
|
|
||||||
#### Scenario: Проверка без записи
|
#### Scenario: Проверка без записи
|
||||||
- **WHEN** `python3 agent.py check`
|
- **WHEN** `python3 agent.py check`
|
||||||
- **THEN** агент печатает доступность хаба (версия настроек) и Jira (`GET /myself`), в Jira ничего не пишет
|
- **THEN** агент печатает доступность хаба (версия настроек) и Jira (`GET /myself`), в Jira ничего не пишет
|
||||||
|
|
||||||
|
#### Scenario: Проверка через папку
|
||||||
|
- **WHEN** `hub_url = "folder:/Volumes/My Shared Files/RSHB/jira-bridge"` и реле хаба работает
|
||||||
|
- **THEN** `check` печатает «хаб: доступен через папку /Volumes/My Shared Files/RSHB/jira-bridge, настройки v<N>, проект
|
||||||
|
<KEY>»; если каталога нет или реле не ответило за 10 с — строка с «НЕДОСТУПЕН» и причиной, код выхода 1
|
||||||
|
|
||||||
### Requirement: Цикл выполнения
|
### Requirement: Цикл выполнения
|
||||||
Агент SHALL работать одним процессом: `claim` к хабу (долгий запрос) → при получении команды: досыпать до
|
Агент SHALL работать одним процессом: `claim` к хабу (долгий запрос) → при получении команды: досыпать до
|
||||||
`pause_seconds` от последнего вызова Jira → запись «started» в журнал → выполнение по контракту `commands` → запись
|
`pause_seconds` от последнего вызова Jira → запись «started» в журнал → выполнение по контракту `commands` → запись
|
||||||
@ -58,10 +64,34 @@ status, result, error, jira_calls, rate_limited}`, `acked {command_id}`. При
|
|||||||
- **THEN** 2 вызова `search` + 1 на соседей; `sync_run.issues_count = 180 + соседи`
|
- **THEN** 2 вызова `search` + 1 на соседей; `sync_run.issues_count = 180 + соседи`
|
||||||
|
|
||||||
### Requirement: Доставка и запуск
|
### Requirement: Доставка и запуск
|
||||||
README агента SHALL описывать: копирование `agent.py` и `agent.example.json` через общую папку, заполнение `agent.json`,
|
README агента SHALL описывать: копирование `agent.py` и `agent.example.json` через общую папку, заполнение `agent.json`
|
||||||
`check`, запуск в терминале и через `nohup` с логом, опциональный launchd-plist, `prune`. Версия агента SHALL быть в
|
(включая выбор транспорта в `hub_url`), `check`, запуск в терминале и через `nohup` с логом, опциональный launchd-plist,
|
||||||
константе файла и в каждом отчёте; хаб предупреждает в `agent_status`, если версия ниже минимальной из настроек.
|
`prune`. Версия агента SHALL быть в константе файла (2 — с транспортом через папку) и в каждом отчёте; хаб предупреждает
|
||||||
|
в `agent_status`, если версия ниже минимальной из настроек.
|
||||||
|
|
||||||
#### Scenario: Устаревший агент
|
#### Scenario: Устаревший агент
|
||||||
- **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2`
|
- **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2`
|
||||||
- **THEN** `agent_status.warning` = «обновите агента», команды выдаются по-прежнему
|
- **THEN** `agent_status.warning` = «обновите агента», команды выдаются по-прежнему
|
||||||
|
|
||||||
|
### Requirement: Транспорт через общую папку
|
||||||
|
При `hub_url = "folder:<путь>"` агент SHALL обращаться к хабу теми же запросами, что по HTTP, через файлы в `<путь>`:
|
||||||
|
на каждый вызов записать `req/<id>.json` = `{id, method, path, body, authorization, deadline}` (`id` — `uuid4().hex`,
|
||||||
|
`authorization` — `Bearer <hub_token>`, `deadline` — момент UTC = сейчас + таймаут вызова; для `claim` таймаут не меньше
|
||||||
|
45 с) через временный файл с именем на точку и переименование, затем опрашивать `resp/<id>.json` каждые 0,1 с.
|
||||||
|
Полученный ответ агент SHALL удалить; `status` 2xx — вернуть разобранное тело, иной — выбросить `HttpError(status, body)`
|
||||||
|
так же, как HTTP-клиент. Если к `deadline` ответа нет, агент SHALL удалить свой `req/<id>.json` (если он ещё там) и
|
||||||
|
выбросить `ConnectionError`. Нечитаемый JSON ответа SHALL перечитываться до 2 с, затем `ConnectionError`. Чужие файлы
|
||||||
|
в `resp/` агент MUST NOT трогать. Цикл выполнения, журнал, повторы отчёта и восстановление SHALL работать без изменений.
|
||||||
|
|
||||||
|
#### Scenario: Ответ хаба через папку
|
||||||
|
- **WHEN** агент вызвал `settings`, реле положило `resp/<id>.json` со `status = 200`
|
||||||
|
- **THEN** агент получает настройки, файл ответа удалён
|
||||||
|
|
||||||
|
#### Scenario: Реле не отвечает
|
||||||
|
- **WHEN** агент вызвал `claim`, а ответа нет 45 с
|
||||||
|
- **THEN** `req/<id>.json` удалён агентом, выброшен `ConnectionError`; цикл пишет «хаб недоступен» и повторяет `claim`
|
||||||
|
через `poll_seconds`, как при недоступном хабе по сети
|
||||||
|
|
||||||
|
#### Scenario: Код ошибки хаба через папку
|
||||||
|
- **WHEN** агент отправил `result` по команде, которой в хабе нет, реле положило ответ со `status = 404`
|
||||||
|
- **THEN** агент получает `HttpError(404)` и считает команду закрытой, как по HTTP
|
||||||
|
|||||||
@ -64,9 +64,15 @@ MUST NOT менять `issue_note`. При завершении `ensure_issue`
|
|||||||
### Requirement: Настройки
|
### Requirement: Настройки
|
||||||
Таблица `setting` SHALL хранить парами ключ–JSON: `base_jql`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
|
Таблица `setting` SHALL хранить парами ключ–JSON: `base_jql`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
|
||||||
`transitions_map`, `pacing`, `board_id`, `sprint_names`, `tokens` (агентский и клиентский), `settings_version`
|
`transitions_map`, `pacing`, `board_id`, `sprint_names`, `tokens` (агентский и клиентский), `settings_version`
|
||||||
(увеличивается при любой правке), `backup_keep`, `auto_refresh_minutes`. Начальные значения SHALL импортироваться из
|
(увеличивается при любой правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или
|
||||||
`admin/jira-sync/config.example.json` командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать.
|
`null`, по умолчанию `null`; относительный путь и не-строка при записи — 422; агенту не передаётся). Начальные значения
|
||||||
|
SHALL импортироваться из `admin/jira-sync/config.example.json` командой `jira-hub init`; пароль и логин Jira в настройках
|
||||||
|
MUST NOT присутствовать.
|
||||||
|
|
||||||
#### Scenario: Правка настроек поднимает версию
|
#### Scenario: Правка настроек поднимает версию
|
||||||
- **WHEN** через API изменён `pacing`
|
- **WHEN** через API изменён `pacing`
|
||||||
- **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию
|
- **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию
|
||||||
|
|
||||||
|
#### Scenario: Неверный relay_dir
|
||||||
|
- **WHEN** `PUT /settings` с `relay_dir = "rshb/jira-bridge"`
|
||||||
|
- **THEN** 422, значение не записано, `settings_version` не изменился
|
||||||
|
|||||||
@ -73,3 +73,46 @@ TBD - created by archiving change bridge-core. Update Purpose after archive.
|
|||||||
#### Scenario: Очередь занята
|
#### Scenario: Очередь занята
|
||||||
- **WHEN** интервал прошёл, но в очереди есть `queued` команды
|
- **WHEN** интервал прошёл, но в очереди есть `queued` команды
|
||||||
- **THEN** автообновление не ставится до опустошения очереди
|
- **THEN** автообновление не ставится до опустошения очереди
|
||||||
|
|
||||||
|
### Requirement: Реле через общую папку
|
||||||
|
`jira-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле
|
||||||
|
бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если
|
||||||
|
каталог `relay_dir` существует, реле SHALL создать в нём подкаталоги `req/`, `resp/`, `work/`; если не существует —
|
||||||
|
записать одно предупреждение в лог и проверять снова.
|
||||||
|
|
||||||
|
Обмен:
|
||||||
|
- запрос агента — файл `req/<id>.json`: `{id, method, path, body, authorization, deadline}`, где `deadline` — момент UTC
|
||||||
|
(ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать;
|
||||||
|
- реле SHALL брать запрос переименованием в `work/<id>.json` и обрабатывать каждый взятый запрос в отдельном потоке;
|
||||||
|
- запрос с истёкшим `deadline` SHALL выбрасываться без ответа; `POST /agent/claim` SHALL пересылаться, только если до
|
||||||
|
`deadline` осталось не меньше 35 с, иначе выбрасываться без ответа;
|
||||||
|
- остальные запросы реле SHALL пересылать в свой HTTP на `127.0.0.1:<port>` тем же методом, путём и телом, с заголовком
|
||||||
|
`Authorization` из файла, и записывать ответ `resp/<id>.json` = `{id, status, body}` (тело — строка ответа HTTP) через
|
||||||
|
временный файл с именем на точку и переименование; затем удалять `work/<id>.json`;
|
||||||
|
- если соединение с `127.0.0.1:<port>` не удалось, ответ SHALL иметь `status = 502` и текст ошибки в `body`;
|
||||||
|
- нечитаемый JSON запроса SHALL перечитываться до 2 с, затем запрос выбрасывается с записью в лог.
|
||||||
|
|
||||||
|
При старте реле SHALL удалить файлы из `work/` с записью в лог. Раз в минуту реле SHALL удалять ответы в `resp/` старше
|
||||||
|
10 мин, запросы в `req/` с `deadline` старше 60 с и файлы с именем на точку старше 10 мин. На каждый обработанный запрос
|
||||||
|
реле SHALL писать в лог хаба строку `relay <METHOD> <path> <status> <секунды>`. HTTP API, обработчики и проверка токенов
|
||||||
|
при этом не меняются.
|
||||||
|
|
||||||
|
#### Scenario: Запрос через папку
|
||||||
|
- **WHEN** в `req/` появился `a1.json` с `GET /agent/settings`, агентским токеном и `deadline` через 60 с
|
||||||
|
- **THEN** в `resp/` появляется `a1.json` со `status = 200` и телом настроек; `work/a1.json` удалён
|
||||||
|
|
||||||
|
#### Scenario: Чужой токен через папку
|
||||||
|
- **WHEN** в файле запроса `GET /issues` с агентским токеном
|
||||||
|
- **THEN** ответ со `status = 401`, как по HTTP
|
||||||
|
|
||||||
|
#### Scenario: Просроченный claim
|
||||||
|
- **WHEN** реле взяло `POST /agent/claim`, до `deadline` которого осталось 20 с
|
||||||
|
- **THEN** запрос выброшен без ответа и без обращения к хабу; команда в очереди осталась `queued`
|
||||||
|
|
||||||
|
#### Scenario: Долгий claim не задерживает другие запросы
|
||||||
|
- **WHEN** реле пересылает `claim`, который ждёт команду 30 с, и в это время в `req/` появляется `GET /agent/settings`
|
||||||
|
- **THEN** ответ на `settings` пишется, не дожидаясь `claim`
|
||||||
|
|
||||||
|
#### Scenario: Папка не подключена
|
||||||
|
- **WHEN** `relay_dir` задан, но каталога нет; через минуту каталог появился
|
||||||
|
- **THEN** в логе одно предупреждение; после появления каталога реле создаёт подкаталоги и обрабатывает запросы
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user