jira-agent/docs/superpowers/plans/2026-10-22-attachment-fetch-agent.md
Andrey Epifancev ceadd9c071 План: вложения из Jira по запросу, агент v14 (#43)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:05:21 +04:00

11 KiB
Raw Blame History

Вложения из 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

  1. Перенаправление 302 на тот же хост (Jira иногда так отдаёт файл) — проходит; на другой хост или схему — failed без запроса туда (тесты в Task 2).
  2. Вложение без size в Jira и содержимое больше предела — чтение обрывается, хабу ничего не уходит (тест в Task 3).
  3. Пустое вложение (0 байт) — done, хаб получил пустой content (тест в Task 3).
  4. Хаб недоступен на POST /agent/files — failed с retryable, как прочие обрывы связи с хабом (тест в Task 3).
  5. Кириллица и пробелы в имени — путь скачивания в 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).