docs(agent): README (доставка, запуск, launchd, prune), архитектура, проверка синтаксиса 3.9
README: доставка на виртуалку, check/запуск/launchd, журнал и prune, поведение с командами, известные ограничения (prune при работе агента, Ctrl-C во время паузы, https без CA, спринты закрытых спринтов, refresh, сравнение описания, 429/503 без потолка). CLAUDE.md и ARCHITECTURE.md заполнены по src/agent.py. tests/test_py39.py: разбор с feature_version=(3, 9) и разрешённый список импортов (только stdlib). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
parent
778c1bee12
commit
54f65d0333
@ -1,15 +1,32 @@
|
||||
# Архитектура
|
||||
|
||||
{{Карта модулей на одну страницу: что где лежит, границы, потоки данных. Читается агентом перед architectural brainstorming. Обновляется вместе со спеками.}}
|
||||
Агент — один файл `src/agent.py` (Python 3.9, только стандартная библиотека). Тесты в `tests/` работают на фейках
|
||||
Jira и хаба. Спека: `../jira-hub/openspec/changes/bridge-core/`, план: `../jira-hub/docs/superpowers/plans/2026-10-11-bridge-core-hub.md`.
|
||||
|
||||
## Модули
|
||||
## Разделы файла `src/agent.py`
|
||||
|
||||
| Модуль | Ответственность | Зависит от |
|
||||
|---|---|---|
|
||||
| | | |
|
||||
| Раздел | Классы и функции | Ответственность | Что использует |
|
||||
|---|---|---|---|
|
||||
| Настройки | `load_config` | читает `agent.json`, проверяет обязательные поля | — |
|
||||
| HTTP | `Http`, `HttpError`, `JiraClient`, `HubClient` | запросы; `JiraClient` считает вызовы, повторяет 429/503 по `Retry-After` (до 5 попыток), хранит время последнего вызова | `urllib` |
|
||||
| Журнал | `Journal`, `_parse_at` | дозапись `journal.jsonl`: `started` → `finished` → `acked`; восстановление; `prune` | файловая система |
|
||||
| Исполнитель | `Executor` и `do_<тип>` | по одному методу на тип команды: читает Jira, пишет только разницу; `refresh` читает и отправляет состояние в хаб | `JiraClient`, `HubClient` (только `refresh`), настройки хаба |
|
||||
| Цикл | `Runner` | claim, пауза до команды, журнал, выполнение, отчёт до подтверждения, восстановление при старте | `HubClient`, `JiraClient`, `Journal`, `Executor` |
|
||||
| CLI | `cmd_run`, `cmd_check`, `main` | команды `run`, `check`, `prune`; обработка сигналов | `Runner`, `Journal` |
|
||||
|
||||
## Границы
|
||||
- Что не должно импортировать что.
|
||||
- Пароль и токен 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, пункт 3).
|
||||
- Защита `prune` от параллельной записи: блокировки нет (ограничение README, пункт 1).
|
||||
- Спринт задач закрытых спринтов: `sprint = null` (рулинг R-6).
|
||||
|
||||
50
CLAUDE.md
50
CLAUDE.md
@ -1,32 +1,44 @@
|
||||
# jira-agent
|
||||
|
||||
## Что это
|
||||
{{ОДНИМ АБЗАЦЕМ: что делает система, для кого, главный сценарий.}}
|
||||
Клиент моста Jira для виртуалки с доступом к Jira банка. Опрашивает хаб `jira-hub` (Mac mini), выполняет команды
|
||||
«желаемый результат» по правилу «проверь, потом сделай», ведёт журнал на диске и отдаёт хабу состояние задач.
|
||||
Главный сценарий: хаб ставит команду (например, создать задачу, перевести статус, перенести в спринт), агент
|
||||
читает задачу в Jira, записывает только разницу и возвращает результат; хаб синхронизирует зеркало через `refresh`.
|
||||
Спека: `../jira-hub/openspec/changes/bridge-core/` (`specs/agent/spec.md`, `specs/commands/spec.md`).
|
||||
|
||||
## Стек и ограничения
|
||||
- {{Язык, сборка, тесты — например: Python 3.12, uv, pytest.}}
|
||||
- {{Жёсткие ограничения среды исполнения — без чего код не должен собираться.}}
|
||||
- Python 3.9.6 (как на виртуалке), только стандартная библиотека, без venv и зависимостей.
|
||||
- Запрещено: синтаксис 3.10+ (`match`, `X | Y` в аннотациях, `dict | dict`), сторонние импорты.
|
||||
Проверяет `tests/test_py39.py` (разбор с `feature_version=(3, 9)` и разрешённый список импортов).
|
||||
- `agent.json` содержит учётные данные Jira — не коммитить, не отправлять в хаб, не выносить с виртуалки.
|
||||
Шаблон без секретов — `src/agent.example.json`.
|
||||
|
||||
## Архитектура
|
||||
- `src/core/` (или {{ваш путь}}) — чистая логика без зависимостей от фреймворка/платформы. Покрывается тестами полностью.
|
||||
- `src/{{adapter}}/` — тонкая обёртка над платформой. Тестируется руками или интеграционно.
|
||||
- {{Ключевые интерфейсы/порты между ними, если есть.}}
|
||||
- `src/agent.py` — весь агент, один файл. Разделы в порядке файла: HTTP-клиенты (`Http`, `JiraClient`, `HubClient`),
|
||||
`Journal`, `Executor` (по методу `do_<тип>` на каждый тип команды), `Runner` (цикл claim → пауза → журнал →
|
||||
выполнение → отчёт), CLI (`cmd_check`, `cmd_run`, `main`).
|
||||
- Границы: `Executor` знает Jira, но не знает хаб (кроме `refresh`, где хаб нужен для `known_keys` и `state`).
|
||||
`Runner` знает и хаб, и Jira, и журнал. Журнал не знает о Jira.
|
||||
- `src/agent.example.json` — шаблон настроек, `README.md` — эксплуатация.
|
||||
- Подробнее: `ARCHITECTURE.md`.
|
||||
|
||||
## Команды
|
||||
- `{{npm test / pytest}}` — весь набор тестов. Зелёным считается только весь набор.
|
||||
- `{{npm run build / ...}}` — сборка.
|
||||
- `{{npm run dev / ...}}` — локальный запуск.
|
||||
- `/usr/bin/python3 -m unittest discover -s tests -t .` — весь набор тестов. Зелёным считается только весь набор.
|
||||
- `/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .` — то же с ошибками на незакрытые ресурсы (так проверяем перед коммитом).
|
||||
- `/usr/bin/python3 src/agent.py check` — доступность хаба и Jira, без записи в Jira.
|
||||
- `/usr/bin/python3 src/agent.py` — основной цикл (нужен `agent.json` рядом с `agent.py`).
|
||||
- `/usr/bin/python3 src/agent.py prune` — очистка журнала (не при работающем агенте).
|
||||
|
||||
## Тесты
|
||||
- Golden-тесты: `tests/fixtures/<name>.*` + эталон `<name>.expected.*`. Внешние сервисы в тестах мокаются.
|
||||
- {{Где лежит тестовое окружение (vault, БД, песочница).}}
|
||||
- Тесты в `tests/`, запуск из корня worktree. Внешних сервисов нет: Jira и хаб заменяют фейки `tests/fake_jira.py` и `tests/fake_hub.py`.
|
||||
- `tests/_common.py` — общая настройка: путь к `src`, `SETTINGS` (как их отдаёт `/agent/settings`), `jira_cfg()`, `no_sleep()`.
|
||||
Паузы в тестах отключаются через `ZERO_PACING` и `no_sleep`.
|
||||
- Для каждого типа команды — свой набор: `test_executor_fields.py`, `test_executor_links.py`, `test_refresh.py`;
|
||||
цикл и восстановление — `test_runner.py`; журнал — `test_journal.py`; HTTP и 429 — `test_clients.py`.
|
||||
- Golden-фикстур пока нет: каждый тест строит сценарий в коде.
|
||||
|
||||
## Первая подсистема
|
||||
{{Что делается первым и почему. Тестовая инфраструктура — Task 1 первого плана.}}
|
||||
|
||||
## OpenSpec и superpowers
|
||||
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.
|
||||
- Когда brainstorming классифицировал задачу как architectural, структуру change создать через `openspec new change <id>` (или `/opsx:propose <id>`), а содержимое proposal.md, specs/ и design.md написать по итогам brainstorming. tasks.md из OpenSpec не заполнять — задачи живут в плане superpowers.
|
||||
- `/opsx:apply` не использовать: исполнение идёт через writing-plans и SDD/executing-plans.
|
||||
- `openspec archive <id>` — после merge PR, как часть чеклиста закрытия.
|
||||
- Хук require-plan защищает любой каталог `src/` на любой глубине. Если код лежит иначе — перечислить каталоги (префиксы от корня, по одному в строке) в `.claude/code-paths`.
|
||||
Исполнитель команд и цикл агента (спека `agent/spec.md`, `commands/spec.md`) — реализованы целиком: все типы команд,
|
||||
журнал, восстановление после перезапуска, CLI `run|check|prune`. Задача 7 плана — документация и проверка совместимости с 3.9.
|
||||
План и спека живут в `jira-hub`: `openspec/changes/bridge-core/` и `docs/superpowers/plans/2026-10-11-bridge-core-hub.md`.
|
||||
|
||||
164
README.md
Normal file
164
README.md
Normal file
@ -0,0 +1,164 @@
|
||||
# jira-agent
|
||||
|
||||
Клиент моста Jira для виртуалки с доступом к Jira банка. Агент опрашивает хаб (`jira-hub` на Mac mini),
|
||||
выполняет команды «желаемый результат» в Jira по правилу «проверь, потом сделай», ведёт журнал на диске
|
||||
и возвращает хабу состояние задач. Один файл `src/agent.py`, только стандартная библиотека, Python 3.9.
|
||||
|
||||
Спека: `../jira-hub/openspec/changes/bridge-core/specs/agent/spec.md` (вместе с `commands/` и `hub-api/`).
|
||||
Хаб, очередь команд и зеркало задач живут в репозитории `jira-hub`; агент с ним общается только по HTTP.
|
||||
|
||||
## Доставка на виртуалку
|
||||
|
||||
1. Скопировать `src/agent.py` и `src/agent.example.json` через общую папку в каталог на виртуалке, например `~/jira-agent/`.
|
||||
2. Переименовать `agent.example.json` в `agent.json` и заполнить:
|
||||
- `hub_url` — `http://192.168.0.106:8790`;
|
||||
- `hub_token` — агентский токен из `jira-hub tokens` на Mac mini;
|
||||
- `jira.base_url` — адрес Jira банка;
|
||||
- `jira.auth` — `{"type": "basic", "user": ..., "password": ...}` или `{"type": "bearer", "token": ...}`;
|
||||
- `verify_ssl`, `ca_bundle` — при корпоративном сертификате (см. ограничение 3 ниже).
|
||||
3. `agent.json` содержит учётные данные Jira. Никуда не копировать и никуда не отправлять, кроме этой виртуалки.
|
||||
Хаб получает от агента только результаты команд и состояние задач, без паролей и токенов Jira.
|
||||
|
||||
Остальные настройки (id полей, типы задач, карта переходов, типы связей, доска, темп) агент получает от хаба при
|
||||
старте и при каждом изменении `settings_version`; в `agent.json` их быть не должно.
|
||||
|
||||
## Проверка
|
||||
|
||||
```
|
||||
python3 agent.py check
|
||||
```
|
||||
|
||||
В норме печатает две строки: `хаб: доступен, настройки v<N>, проект <KEY>` и `Jira: доступен, пользователь <login>`.
|
||||
Код выхода 0. Если хаб или Jira недоступны, строка содержит `НЕДОСТУПЕН` и текст ошибки, код выхода 1.
|
||||
Команда ничего не пишет в Jira.
|
||||
|
||||
## Запуск
|
||||
|
||||
В терминале:
|
||||
|
||||
```
|
||||
python3 agent.py
|
||||
```
|
||||
|
||||
В фоне с логом:
|
||||
|
||||
```
|
||||
nohup python3 -u agent.py > agent.log 2>&1 &
|
||||
```
|
||||
|
||||
Остановка: Ctrl-C или `kill <pid>`. Первый сигнал — агент доделывает текущую команду, отправляет отчёт и выходит.
|
||||
Повторный сигнал — немедленный выход (код 130). Команда, которая не успела записаться в журнал `started`, при этом
|
||||
остаётся `claimed` в хабе (см. ограничение 2).
|
||||
|
||||
## Автозапуск через launchd (опционально)
|
||||
|
||||
Пример `~/Library/LaunchAgents/ru.aepif.jira-agent.plist`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>ru.aepif.jira-agent</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/python3</string>
|
||||
<string>-u</string>
|
||||
<string>/Users/USER/jira-agent/agent.py</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key><string>/Users/USER/jira-agent</string>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>KeepAlive</key><true/>
|
||||
<key>StandardOutPath</key><string>/Users/USER/jira-agent/agent.log</string>
|
||||
<key>StandardErrorPath</key><string>/Users/USER/jira-agent/agent.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
Подставить свой путь вместо `USER`. Запуск и остановка:
|
||||
|
||||
```
|
||||
launchctl load ~/Library/LaunchAgents/ru.aepif.jira-agent.plist
|
||||
launchctl unload ~/Library/LaunchAgents/ru.aepif.jira-agent.plist
|
||||
```
|
||||
|
||||
`WorkingDirectory` нужен, чтобы `agent.json` и `journal.jsonl` лежали рядом с `agent.py`.
|
||||
|
||||
## Журнал
|
||||
|
||||
`journal.jsonl` лежит рядом с `agent.py`. Это только дозапись: на каждую команду пишутся `started` (после паузы,
|
||||
перед обращением к Jira), `finished` (результат и счётчики вызовов) и `acked` (хаб подтвердил приём отчёта).
|
||||
По журналу агент после перезапуска досылает неподтверждённые отчёты и находит команды, начатые, но не завершённые.
|
||||
|
||||
Журнал содержит параметры команд, то есть тексты задач и комментариев. Не выносить его с виртуалки.
|
||||
|
||||
Очистка подтверждённых записей старше 30 дней:
|
||||
|
||||
```
|
||||
python3 agent.py prune
|
||||
```
|
||||
|
||||
Не запускать при работающем агенте (ограничение 1 ниже).
|
||||
|
||||
## Что делает агент с командами
|
||||
|
||||
Типы команд описаны в спеке `commands/spec.md`. Здесь — что агент делает с каждым:
|
||||
|
||||
| Тип | Что делает |
|
||||
|---|---|
|
||||
| `ensure_issue` | ищет задачу своей темы (`reporter = currentUser()`, за 7 дней), иначе создаёт; возвращает `key` |
|
||||
| `ensure_fields` | читает задачу и пишет только отличающиеся поля |
|
||||
| `ensure_comment` | комментарий с маркером в начале: обновляет, если текст изменился, иначе создаёт |
|
||||
| `ensure_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных |
|
||||
| `ensure_link` | создаёт связь; конфликт без `replace` — `failed`; с `replace` сначала удаляет лишнее |
|
||||
| `ensure_sprint` | переносит в спринт доски (активные и будущие) или в бэклог |
|
||||
| `refresh` | только читает Jira и отправляет состояние задач в хаб; ничего не пишет |
|
||||
|
||||
Правила работы:
|
||||
- **Проверь, потом сделай.** Каждый тип сначала читает текущее состояние и записывает только разницу.
|
||||
- **Паузы.** Хаб выдаёт команду с `pause_seconds` — агент досыпает до зазора от последнего вызова Jira. Внутри команды
|
||||
с несколькими записями зазор берётся из `pacing` настроек хаба.
|
||||
- **Одна команда в полёте.** Агент берёт следующую команду, только когда предыдущая выполнена и отчёт подтверждён.
|
||||
- **Отчёт до подтверждения.** Отчёт повторяется каждые 10 с, пока хаб не подтвердит. Ответы 404/409/422 хаб
|
||||
выдаёт, когда команда уже отменена или удалена: агент считает её закрытой и не зацикливается.
|
||||
- **429 и 503.** Агент ждёт `Retry-After` из ответа Jira и повторяет запрос, до 5 попыток. Если заголовка нет,
|
||||
ждёт 60 с, умноженные на номер попытки. Число ожиданий идёт в `rate_limited` отчёта.
|
||||
- **Версия.** Константа `AGENT_VERSION` в `agent.py` (сейчас 1) отправляется в каждом `claim`.
|
||||
Если хаб назначил `min_agent_version` выше, он пишет в `agent_status.warning`: «обновите агента»; команды
|
||||
при этом выдаются по-прежнему.
|
||||
|
||||
## Тесты
|
||||
|
||||
```
|
||||
/usr/bin/python3 -m unittest discover -s tests -t .
|
||||
```
|
||||
|
||||
Тот же Python 3.9.6, что на виртуалке. Внешних сервисов нет: Jira и хаб заменяют фейки
|
||||
(`tests/fake_jira.py`, `tests/fake_hub.py`). `tests/test_py39.py` проверяет, что `agent.py` разбирается как Python 3.9
|
||||
и импортирует только стандартную библиотеку.
|
||||
|
||||
## Обновление агента
|
||||
|
||||
Скопировать новый `agent.py` поверх старого и перезапустить процесс (или `launchctl unload` + `load`).
|
||||
`agent.json` и `journal.jsonl` не трогать: журнал переживает обновление, а неподтверждённые отчёты досылаются при старте.
|
||||
|
||||
## Известные ограничения / эксплуатация
|
||||
|
||||
В порядке важности:
|
||||
|
||||
1. **`prune` не запускать при работающем агенте.** Блокировки нет. Запись, добавленная агентом во время `prune`,
|
||||
может пропасть при перезаписи файла. Сначала остановить агента.
|
||||
2. **Ctrl-C во время паузы перед командой.** Команда остаётся `claimed` в хабе, в журнале о ней ничего нет.
|
||||
Хаб сам её не переиздаёт. Лечение: в хабе отменить команду с force и повторить:
|
||||
`POST /commands/{id}/cancel` с телом `{"force": true}`, затем `POST /commands/{id}/retry`.
|
||||
3. **HTTPS хаба с корпоративным CA не поддержан.** `HubClient` работает без `ca_bundle` и `verify_ssl`: `hub_url`
|
||||
должен быть `http://`. Для Jira `verify_ssl` и `ca_bundle` работают.
|
||||
4. **Спринт в `refresh` — только активные и будущие спринты доски.** Поле `sprint` берётся из Agile API по
|
||||
этим спринтам (рулинг R-6). У задач закрытых спринтов `sprint = null`.
|
||||
5. **Особенности `refresh`.** В `result.issues` попадают только запрошенные задачи, соседи по связям — в
|
||||
`result.neighbors`. Если `jql` вернул пустую выдачу, состояние в хаб не отправляется (`pages = 0`).
|
||||
Отсутствующие ключи при `refresh` по `keys` попадают в `result.missing` (не больше 50).
|
||||
6. **Сравнение описания в `ensure_fields` точное.** Если Jira нормализует переводы строк (CRLF), будут лишние
|
||||
записи. Наблюдать на реальной Jira.
|
||||
7. **429/503 без потолка.** Агент ждёт `Retry-After` сколько бы он ни был (без верхней границы), до 5 попыток.
|
||||
Длинный `Retry-After` держит очередь.
|
||||
26
tests/test_py39.py
Normal file
26
tests/test_py39.py
Normal file
@ -0,0 +1,26 @@
|
||||
import ast
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from _common import SRC
|
||||
|
||||
|
||||
class Py39Test(unittest.TestCase):
|
||||
def test_agent_parses_as_python_39(self):
|
||||
with open(os.path.join(SRC, "agent.py"), encoding="utf-8") as f:
|
||||
ast.parse(f.read(), feature_version=(3, 9))
|
||||
|
||||
def test_no_third_party_imports(self):
|
||||
with open(os.path.join(SRC, "agent.py"), encoding="utf-8") as f:
|
||||
tree = ast.parse(f.read())
|
||||
allowed = {"argparse", "base64", "json", "os", "signal", "ssl", "sys", "time", "urllib", "datetime", "typing"}
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
self.assertIn(a.name.split(".")[0], allowed, a.name)
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
self.assertIn((node.module or "").split(".")[0], allowed, node.module)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
x
Reference in New Issue
Block a user