Рулинг R-3 дописан в design.md; CLAUDE.md больше не ссылается на активный change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
9.2 KiB
Markdown
98 lines
9.2 KiB
Markdown
# agent Specification
|
||
|
||
## Purpose
|
||
TBD - created by archiving change bridge-core. Update Purpose after archive.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Состав и настройки агента
|
||
Агент SHALL состоять из одного файла `agent.py` (Python ≥ 3.9, только стандартная библиотека) и файла `agent.json`:
|
||
`hub_url`, `hub_token`, `jira.base_url`, `jira.auth` (`basic` с `user`/`password` или `bearer` с `token`), `verify_ssl`,
|
||
`ca_bundle`, `timeout`, `poll_seconds`. `hub_url` вида `http://…` или `https://…` SHALL означать HTTP-транспорт, вида
|
||
`folder:<абсолютный путь>` — транспорт через общую папку. Всё остальное (id полей, типы, карта переходов, типы связей,
|
||
доска, темп) агент SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT
|
||
отправляться в хаб и MUST NOT попадать в журнал, логи и общую папку.
|
||
|
||
#### Scenario: Проверка без записи
|
||
- **WHEN** `python3 agent.py check`
|
||
- **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: Цикл выполнения
|
||
Агент SHALL работать одним процессом: `claim` к хабу (долгий запрос) → при получении команды: досыпать до
|
||
`pause_seconds` от последнего вызова Jira → запись «started» в журнал → выполнение по контракту `commands` → запись
|
||
«finished» с результатом → отчёт хабу, повторяемый до подтверждения. Одна команда в полёте. При изменении
|
||
`settings_version` в ответе `claim` агент SHALL перечитать настройки до выполнения команды.
|
||
|
||
#### Scenario: Остановка между командами
|
||
- **WHEN** получен Ctrl-C во время выполнения команды
|
||
- **THEN** текущая команда доделывается и отчёт отправляется, затем процесс завершается
|
||
|
||
### Requirement: Журнал на диске и восстановление
|
||
Агент SHALL вести `journal.jsonl` (только дозапись) рядом с собой: строки `started {command_id, at}`, `finished {command_id,
|
||
status, result, error, jira_calls, rate_limited}`, `acked {command_id}`. При старте агент SHALL: отправить хабу все
|
||
`finished` без `acked`; для `started` без `finished` — выполнить команду заново по правилу «проверь, потом сделай»
|
||
(запросив её параметры у хаба по id). Команда `python3 agent.py prune` SHALL удалять из журнала записи с `acked` старше
|
||
30 дней.
|
||
|
||
#### Scenario: Падение после ответа Jira
|
||
- **WHEN** агент упал между `POST /issue` и записью `finished`, затем перезапущен
|
||
- **THEN** агент находит в журнале `started` без `finished`, запрашивает команду у хаба, находит созданную задачу поиском
|
||
дубля и отчитывается `done` с её ключом; второй задачи нет
|
||
|
||
### Requirement: Ограничение темпа и 429
|
||
Агент SHALL помнить время последнего вызова Jira (любого) и не начинать команду раньше `pause_seconds` после него.
|
||
На HTTP 429 и 503 агент SHALL ждать `Retry-After` (иначе 60 с × номер попытки) и повторять тот же запрос до 5 раз,
|
||
считая попытки в `rate_limited`; после 5 неудач — `failed`. Пауза между записями внутри одной команды — по зазору типа команды.
|
||
|
||
#### Scenario: 429 с Retry-After
|
||
- **WHEN** Jira ответила 429 с `Retry-After: 90`
|
||
- **THEN** агент ждёт 90 с, повторяет тот же запрос; в отчёте `rate_limited = 1`
|
||
|
||
### Requirement: Чтение состояния
|
||
`refresh` SHALL выполняться запросом `search` (`jql`, `fields` = поля зеркала включая `issuelinks` и спринт,
|
||
`maxResults = 100`, постранично) и отправляться хабу пачками по 100 задач со связями; ключи из `issuelinks`, которых нет
|
||
в выборке и в зеркале хаба (хаб сообщает список известных), агент SHALL дочитать одним `search` по `key in (...)`
|
||
и отправить с признаком `neighbor`.
|
||
|
||
#### Scenario: Полная синхронизация
|
||
- **WHEN** `refresh {jql: "project = AURORA AND labels = AWPCA"}` при 180 задачах
|
||
- **THEN** 2 вызова `search` + 1 на соседей; `sync_run.issues_count = 180 + соседи`
|
||
|
||
### Requirement: Доставка и запуск
|
||
README агента SHALL описывать: копирование `agent.py` и `agent.example.json` через общую папку, заполнение `agent.json`
|
||
(включая выбор транспорта в `hub_url`), `check`, запуск в терминале и через `nohup` с логом, опциональный launchd-plist,
|
||
`prune`. Версия агента SHALL быть в константе файла (2 — с транспортом через папку) и в каждом отчёте; хаб предупреждает
|
||
в `agent_status`, если версия ниже минимальной из настроек.
|
||
|
||
#### Scenario: Устаревший агент
|
||
- **WHEN** агент версии 1 выходит на связь, а в настройках `min_agent_version = 2`
|
||
- **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
|