11 KiB
Вложения из Jira по запросу (агент v14) — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: агент v14 в refresh присылает список вложений задач (без содержимого) и выполняет новую команду
fetch_attachment: скачивает файл вложения из Jira и передаёт хабу POST /agent/files.
Architecture: список — поле attachment в search и Executor.attachments_of(raw) в to_state; скачивание —
JiraClient.download (байты, предел, без перенаправлений на другой хост) и Executor.do_fetch_attachment; отправка —
HubClient.put_file (работает и через FolderTransport). Python 3.9, только stdlib.
Tech Stack: Python 3.9 (/usr/bin/python3), stdlib, unittest, фейки tests/fake_jira.py, tests/fake_hub.py.
Spec: /Users/andrey.epifancev/projects/jira-bridge/jira-hub/.wt/attach-fetch/openspec/changes/attachment-fetch/ —
design.md (решения 1, 4–6, 8, 17, 18), specs/agent/spec.md («Чтение состояния», «Скачивание вложения»),
specs/commands/spec.md («Скачивание вложения по запросу»), specs/hub-api/spec.md (POST /agent/files). Решения агента —
openspec/changes/agent-v14-attachment-fetch/design.md этого репозитория (таблица R-n).
Worktree: /Users/andrey.epifancev/projects/jira-bridge/jira-agent/.wt/attach-fetch, ветка feat/43-attachment-fetch
(aep/jira-agent#43). Все команды — из корня worktree.
Global Constraints
AGENT_VERSION = 14.- Python 3.9 stdlib; без
match, безX | Yв аннотациях (tests/test_py39.py). - Предел —
MAX_ATTACHMENT = 10 * 1024 * 1024(10 485 760 байт). - Адрес содержимого —
<base_url>/secure/attachment/<id>/<urllib.parse.quote(filename, safe="")>;contentиз ответа Jira не используется. - Таймаут скачивания —
max(self.timeout, 120). - Элемент
attachments:{id, filename, size, mime_type, author, created}(author—displayName, иначеname, иначеNone). - Тексты ошибок (точно): «вложение # удалено из в Jira»; «файл больше 10 МБ (<размер>) — скачайте в Jira» (неизвестный размер — «больше 10,0 МБ»); «Jira перенаправила скачивание на <хост>»; «файл пришёл не целиком (<получено> из <размер>)». Размер — «N Б» / «N,N КБ» / «N,N МБ» по 1024, запятая.
- Содержимое файла не попадает в лог, журнал и текст ошибок.
- Каждый коммит — с
(#43)в заголовке и последней строкойCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. - Зелёный — весь набор:
/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t . - Решения вне спеки и плана — в таблицу
openspec/changes/agent-v14-attachment-fetch/design.md; находки ревью задачи n —T<n>.<k>.
Review Focus
- Перенаправление 302 на тот же хост (Jira иногда так отдаёт файл) — проходит; на другой хост или схему —
failedбез запроса туда (тесты в Task 2). - Вложение без
sizeв Jira и содержимое больше предела — чтение обрывается, хабу ничего не уходит (тест в Task 3). - Пустое вложение (0 байт) —
done, хаб получил пустойcontent(тест в Task 3). - Хаб недоступен на
POST /agent/files—failedсretryable, как прочие обрывы связи с хабом (тест в Task 3). - Кириллица и пробелы в имени — путь скачивания в URL-кодировке, фейк Jira находит файл (тест в Task 3).
Task 1: Список вложений в состоянии и версия 14
Files:
- Modify:
src/agent.py—AGENT_VERSION = 14,STATE_FIELDS+"attachment",Executor.attachment_state(a) -> dict(статический),Executor.attachments_of(raw) -> List[dict],to_stateдобавляетattachments - Test:
tests/test_refresh.py
Interfaces:
-
Produces:
attachments_of(raw)— вложения в порядке Jira, безid— отброшено сlog(ключ задачи); нет поляattachment—[]. -
Step 1: Тесты —
test_refresh_attachments_in_state(сценарий «Вложения в состоянии» из дельтыagent: два вложения,author = "Потапова Анна",size = 240000,mime_type = "image/png"; число вызововsearchпрежнее; к содержимому обращений нет — фейк считает GET/secure/attachment/),test_refresh_attachment_without_id_dropped,test_refresh_no_attachments_empty_list. -
Step 2: Прогон —
/usr/bin/python3 -m unittest tests.test_refresh -q→ FAIL. -
Step 3: Реализация по сигнатурам.
-
Step 4: Прогон — весь набор → PASS.
-
Step 5: Commit —
Агент v14: список вложений в состоянии refresh (#43).
Task 2: Скачивание байтов из Jira и файл хабу
Files:
- Modify:
src/agent.py—Http.fetch_bytes(path, limit, timeout) -> bytes(GET, без разбора JSON; перенаправления — свойHTTPRedirectHandler: тот же схема+хост+порт, что уbase, — идти; иначеRedirectAway(host));JiraClient.download(path, limit) -> bytes(через_retry, счётчик вызовов); классRedirectAway(Exception)с полемhost;TooBig(Exception);HubClient.put_file(data: bytes) -> dict(POST /agent/files {content: base64}) - Modify:
tests/fake_jira.py—GET /secure/attachment/<id>/<имя>отдаётdataвложения (имя сверяется после раскодирования), счётчикself.content_gets;self.redirect_content = None(адрес для 302);self.hide_size = set()(id безsizeв JSON) - Modify:
tests/fake_hub.py—POST /agent/filesкладёт вself.files, отвечает{file, size};self.reject_files = None(код ответа или"drop") - Test:
tests/test_clients.py
Interfaces:
-
Produces:
JiraClient.download(path: str, limit: int) -> bytes— читает не большеlimit + 1байт; если прочитано большеlimit—TooBig; 429/503 — повтор как уcall;RedirectAwayиTooBigнаружу. -
Step 1: Тесты —
test_download_bytes(тело как есть,calls+1, заголовокAuthorizationушёл);test_download_redirect_same_host(302 на тот же хост → байты);test_download_redirect_other_host(302 наhttp://127.0.0.2:<порт>/x→RedirectAway, второй сервер запроса не получил);test_download_limit(limit=10, тело 20 байт →TooBig);test_download_429_retry;test_put_file_httpиtest_put_file_folder(черезFolderTransportиtests/fake_relay.py). -
Step 2: Прогон —
/usr/bin/python3 -m unittest tests.test_clients -q→ FAIL. -
Step 3: Реализация. Обработчик перенаправлений —
urllib.request.build_opener(_SameHostRedirect(base), HTTPSHandler(context=ctx));urlopenвHttp.sendне трогать. -
Step 4: Прогон — весь набор → PASS.
-
Step 5: Commit —
Агент: скачивание байтов из Jira без чужих перенаправлений, файл хабу (#43).
Task 3: Команда fetch_attachment
Files:
- Modify:
src/agent.py—Executor.do_fetch_attachment(p, cmd) -> dict,_fmt_size(n: Optional[int]) -> str; ARCHITECTURE.md (раздел «Исполнитель»,JiraClient.download,HubClient.put_file),CLAUDE.md(границаExecutorзнает хаб и вfetch_attachment; последний change) - Test:
tests/test_executor_attachments.py
Interfaces:
-
Consumes:
JiraClient.download,RedirectAway,TooBig,HubClient.put_file(Task 2);self._hub(fn, *args)для вызова хаба. -
Produces: результат
done—{"attachment_id", "file", "size"};fileиsize— из ответа хаба. -
Step 1: Тесты —
test_fetch_screenshot(сценарий «Скачивание скриншота»: имя «скрин 1.png», фейк Jira отдал файл, хаб получил те же байты, результатdoneс sha256 и размером, в журнале агента нет содержимого);test_fetch_deleted(«вложение #31337 удалено из AURORA-1650 в Jira», содержимое не запрашивалось);test_fetch_too_large_known(size 25 000 000 в JSON → «файл больше 10 МБ (23,8 МБ) — скачайте в Jira»,content_gets == 0);test_fetch_too_large_unknown(hide_size, содержимое 12 МБ → «файл больше 10 МБ (больше 10,0 МБ) — скачайте в Jira», хаб ничего не получил);test_fetch_redirect_away(«Jira перенаправила скачивание на 127.0.0.2»);test_fetch_truncated(фейк отдаёт меньше байт, чемsize→ «файл пришёл не целиком (N из M)»);test_fetch_empty(0 байт →done,size = 0);test_fetch_hub_down(reject_files = "drop"→failed,retryable,peer = "хабом");test_fetch_hub_422(ошибка с «хаб: »). -
Step 2: Прогон —
/usr/bin/python3 -m unittest tests.test_executor_attachments -q→ FAIL. -
Step 3: Реализация по контракту
commands«Скачивание вложения по запросу» (шаги 1–5); лог —log(" вложение #%s «%s» (%s) ← %s" % (...)). -
Step 4: Прогон — весь набор с
-W error::ResourceWarning→ PASS. -
Step 5: Commit —
Агент v14: команда fetch_attachment (#43).