jira-agent/docs/superpowers/plans/2026-10-15-agent-v10-comments.md
Andrey Epifancev 92679fdf3a docs: план agent-v10-comments (#27)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:52:09 +04:00

344 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# agent-v10-comments — агент v10: комментарии задач в состоянии — план
> **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:** `refresh` агента передаёт хабу комментарии каждой задачи (`comments: [{id, author, created, updated, body}]`),
чтобы карточка задачи веб-приложения хаба (aep/jira-hub#37) могла их показать; выпуск — агент v10.
**Architecture:** `src/agent.py` — поле `comment` в `Executor.STATE_FIELDS` (тот же `search`, без новых вызовов Jira);
`Executor.comment_state` (форма одного комментария) и `Executor.comments_of` (комментарии из ответа `search`, при
неполном списке — дочитывание `GET /issue/{key}/comment` по 100); `do_refresh` кладёт `comments` в состояние каждой
задачи. Фейк `tests/fake_jira.py` отдаёт `fields.comment` как Jira Server (с `total`) и умеет обрезать список в поиске.
**Tech Stack:** Python 3.9.6, только стандартная библиотека; `unittest`.
**Spec:** `../jira-hub/openspec/changes/issue-card-modal/` — `specs/agent/spec.md` (MODIFIED «Чтение состояния»),
`design.md` решения 1–4 и R-1 (worktree спеки: `/Users/andrey.epifancev/projects/awp-ca/jira-hub/.wt/issue-modal`).
Решения агентской части — `openspec/changes/agent-v10-comments/design.md` (создаётся в Task 2).
**Worktree:** `/Users/andrey.epifancev/projects/awp-ca/jira-agent/.wt/agent-v10`, ветка `feat/27-agent-v10-comments`.
Все команды — из корня worktree.
## Global Constraints
- Python 3.9.6 (`/usr/bin/python3`), только стандартная библиотека; синтаксис 3.10+ запрещён (`tests/test_py39.py`).
- Тесты: `/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .` — зелёным считается только весь набор.
- `AGENT_VERSION = 10`.
- Комментарий в состоянии: `id` — строка; `author` — `displayName`, иначе `name`, иначе `None`; `created`, `updated` —
строки Jira как есть; `body` — текст как есть, пустой — `""`. Порядок — как отдала Jira.
- Дочитывание: `GET /issue/{key}/comment`, `startAt`, `maxResults = 100`, только если в ответе `search`
`len(comment.comments) < comment.total`.
- `comments` есть у каждой задачи состояния, в том числе у соседей; нет комментариев — `[]`.
- Коммиты на русском, трейлер `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
## Review Focus
1. Ответ `search` без `fields.comment` вовсе (поле недоступно по правам) — `comments = []`, `refresh` не падает (Task 1, тест `test_comments_field_missing`).
2. `comment.total` отсутствует или `null` — считать полным полученный список, без дочитывания (Task 1, тот же тест).
3. Автор комментария удалён из Jira (`author` = `null`) — `author = None`, текст на месте (Task 1, `test_comments_in_state`).
4. Комментарий с пустым или отсутствующим `body` — `""`, не `None` (Task 1, `test_comments_in_state`).
5. Дочитывание вернуло 429 — повтор по общему правилу `JiraClient`, как у любого вызова (покрыто `test_clients.py`; отдельного теста не нужно, вызов идёт через `self.jira.get`).
---
### Task 1: Комментарии в состоянии `refresh`
**Модель исполнителя:** haiku (готовый код).
**Files:**
- Modify: `tests/fake_jira.py` (комментарии с автором и временем, `fields.comment` в `issue_json`, обрезка в поиске)
- Modify: `src/agent.py:829-830` (`STATE_FIELDS`), раздел `# --- refresh` (`comment_state`, `comments_of`), `do_refresh`
- Test: `tests/test_refresh.py`
**Interfaces:**
- Produces: `Executor.comment_state(c: dict) -> dict`, `Executor.comments_of(raw: dict) -> List[dict]`; у каждой задачи
в `POST /agent/state` поле `comments`.
- Фейк: `FakeJira.add_comment(key, body, name="ivanov", display="Иванов Иван", created=..., updated=None) -> dict`,
`FakeJira.search_comment_cap: Optional[int]`.
- [ ] **Step 1: Фейк Jira — комментарии как в Jira Server**
В `tests/fake_jira.py`, в `__init__` после `self.drop_next_post_issue = False` добавить:
```python
self.search_comment_cap = None # сколько комментариев задачи отдаёт search (None — все), как обрезка Jira
```
Добавить метод после `add`:
```python
def add_comment(self, key, body, name="ivanov", display="Иванов Иван", created="2026-10-14T12:00:00.000+0300",
updated=None):
author = {k: v for k, v in (("name", name), ("displayName", display)) if v is not None} if name or display else None
self.comment_seq += 1
c = {"id": str(self.comment_seq), "body": body, "author": author, "created": created, "updated": updated or created}
self.comments.setdefault(key, []).append(c)
return c
```
Заменить `issue_json` целиком:
```python
def issue_json(self, key, fields=None):
"""fields — список запрошенных полей (как параметр fields у search); None — все поля, как GET /issue/{key}."""
f = self.issues[key]
out = {"summary": f["summary"], "description": f["description"], "status": {"name": f["status"]},
"priority": {"name": f["priority"], "id": int(f["priority"]) if str(f["priority"]).isdigit() else f["priority"]}, "assignee": self._user(f["assignee"]), "reporter": self._user(f["reporter"]),
"duedate": f["duedate"], "labels": list(f["labels"]), "issuetype": {"name": f["issuetype"]},
"created": f["created"], "updated": f["updated"], "issuelinks": self.links_of(key)}
out.update({c: f.get(c) for c in CUSTOM})
if fields is None or "comment" in fields:
lst = self.comments.get(key, [])
cap = self.search_comment_cap if fields is not None else None
shown = lst if cap is None else lst[:cap]
out["comment"] = {"startAt": 0, "maxResults": len(shown) if cap is None else cap, "total": len(lst),
"comments": shown}
return {"key": key, "id": f["id"], "fields": out}
```
В `route`, ветка `/rest/api/2/search`, заменить строку возврата:
```python
fields = (q.get("fields") or "").split(",") if q.get("fields") else []
return 200, {"startAt": start, "maxResults": maxr, "total": len(keys),
"issues": [self.issue_json(k, fields) for k in page]}
```
В ветке `POST .../comment` (создание комментария) заменить две строки
```python
self.comment_seq += 1
lst.append({"id": str(self.comment_seq), "body": body["body"]})
```
на
```python
self.comment_seq += 1
ts = self.ts(self.now)
lst.append({"id": str(self.comment_seq), "body": body["body"], "author": self._user("me"),
"created": ts, "updated": ts})
```
- [ ] **Step 2: Падающие тесты**
В `tests/test_refresh.py`, в класс `RefreshTest`, после `test_refresh_empty_keys` добавить:
```python
def test_comments_in_state(self):
self.fake.add("AURORA-1")
self.fake.add("AURORA-2")
self.fake.add_comment("AURORA-1", "первый\nвторая строка", name="petrov", display="Петров Пётр",
created="2026-10-14T10:00:00.000+0300")
self.fake.add_comment("AURORA-1", "второй", name="ivanov", display=None, created="2026-10-14T11:00:00.000+0300",
updated="2026-10-14T12:00:00.000+0300")
self.fake.add_comment("AURORA-1", None, name=None, display=None) # автор удалён, текста нет
out = self.ex.run({"id": 8, "type": "refresh", "params": {"keys": ["AURORA-1", "AURORA-2"]}})
self.assertEqual(out["status"], "done")
by_key = {i["key"]: i for i in self.hub.states[0]["issues"]}
c = by_key["AURORA-1"]["comments"]
self.assertEqual([x["author"] for x in c], ["Петров Пётр", "ivanov", None])
self.assertEqual(c[0], {"id": c[0]["id"], "author": "Петров Пётр", "created": "2026-10-14T10:00:00.000+0300",
"updated": "2026-10-14T10:00:00.000+0300", "body": "первый\nвторая строка"})
self.assertIsInstance(c[0]["id"], str)
self.assertEqual(c[1]["updated"], "2026-10-14T12:00:00.000+0300")
self.assertEqual(c[2]["body"], "")
self.assertEqual(by_key["AURORA-2"]["comments"], [])
self.assertIn("comment", self.ex.state_fields().split(","))
searches = [r for r in self.fake.requests if r[1] == "/rest/api/2/search"]
self.assertEqual(len(searches), 1) # комментарии пришли тем же search
self.assertFalse(any(r[1].endswith("/comment") for r in self.fake.requests))
def test_comments_partial_list_is_read_fully(self):
self.fake.add("AURORA-1")
for n in range(130):
self.fake.add_comment("AURORA-1", "к%d" % n)
self.fake.search_comment_cap = 20
out = self.ex.run({"id": 9, "type": "refresh", "params": {"keys": ["AURORA-1"]}})
self.assertEqual(out["status"], "done")
c = self.hub.states[0]["issues"][0]["comments"]
self.assertEqual(len(c), 130)
self.assertEqual([x["body"] for x in c[:2]] + [c[-1]["body"]], ["к0", "к1", "к129"])
reads = [r for r in self.fake.requests if r == ("GET", "/rest/api/2/issue/AURORA-1/comment")]
self.assertEqual(len(reads), 2) # 100 + 30
def test_comments_field_missing(self):
raw = {"key": "AURORA-1", "fields": {}} # поле comment недоступно
self.assertEqual(self.ex.comments_of(raw), [])
raw = {"key": "AURORA-1", "fields": {"comment": {"comments": [{"id": "5", "body": "x"}]}}} # total нет
self.assertEqual(self.ex.comments_of(raw),
[{"id": "5", "author": None, "created": None, "updated": None, "body": "x"}])
self.assertEqual(self.fake.requests, []) # без дочитывания
```
- [ ] **Step 3: Тесты падают**
Run: `/usr/bin/python3 -m unittest tests.test_refresh -v`
Expected: FAIL — `KeyError: 'comments'` в `test_comments_in_state` и `test_comments_partial_list_is_read_fully`,
`AttributeError: 'Executor' object has no attribute 'comments_of'` в `test_comments_field_missing`.
- [ ] **Step 4: Реализация в агенте**
В `src/agent.py` заменить `STATE_FIELDS`:
```python
STATE_FIELDS = ["summary", "description", "status", "priority", "assignee", "reporter", "duedate", "labels",
"issuetype", "created", "updated", "issuelinks", "comment"]
```
После метода `links_from` добавить:
```python
@staticmethod
def comment_state(c: dict) -> dict:
"""Комментарий для хаба: автор — отображаемое имя, иначе логин; текст и время — как отдала Jira."""
a = c.get("author") or {}
return {"id": str(c.get("id")), "author": a.get("displayName") or a.get("name") or None,
"created": c.get("created"), "updated": c.get("updated"), "body": c.get("body") or ""}
def comments_of(self, raw: dict) -> List[dict]:
"""Комментарии задачи из ответа search. Если Jira отдала не все (len < total) — дочитать постранично
через /issue/{key}/comment и взять полный список вместо частичного."""
block = (raw.get("fields") or {}).get("comment") or {}
got = list(block.get("comments") or [])
total = block.get("total")
if total is not None and len(got) < int(total):
got, start = [], 0
while True:
page = self.jira.get(API + "/issue/%s/comment" % raw["key"], startAt=start, maxResults=100)
part = page.get("comments") or []
got += part
start += len(part)
if not part or page.get("total") is None or start >= int(page["total"]):
break
return [self.comment_state(c) for c in got]
```
В `do_refresh`, в цикле `for r in everything:` после строки `st["sprint"] = names.get(r["key"])` добавить:
```python
st["comments"] = self.comments_of(r)
```
- [ ] **Step 5: Тесты проходят, весь набор зелёный**
Run: `/usr/bin/python3 -m unittest tests.test_refresh -v`
Expected: PASS (все тесты `RefreshTest`, включая три новых).
Run: `/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .`
Expected: `OK` (ни одного FAIL/ERROR; `test_executor_fields` с `ensure_comment` — тоже зелёный: форма созданного фейком
комментария дополнена, но `body` и `id` прежние).
- [ ] **Step 6: Commit**
```bash
git add src/agent.py tests/fake_jira.py tests/test_refresh.py
git commit -m "feat(agent): комментарии задач в состоянии refresh (#27)
Поле comment в том же search; при неполном списке — дочитывание /issue/{key}/comment по 100.
Спека: jira-hub/openspec/changes/issue-card-modal/ (agent, «Чтение состояния»).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 2: Версия 10, документация, change решений агента
**Модель исполнителя:** haiku (готовый текст).
**Files:**
- Modify: `src/agent.py:36` (`AGENT_VERSION`)
- Modify: `tests/test_folder.py:161`
- Modify: `README.md` (раздел особенностей `refresh`, пункт 4)
- Modify: `CLAUDE.md` (раздел «Первая подсистема» — последний change)
- Create: `openspec/changes/agent-v10-comments/design.md` (через `openspec new change agent-v10-comments`)
**Interfaces:**
- Consumes: Task 1 (`comments` в состоянии).
- Produces: `AGENT_VERSION = 10`.
- [ ] **Step 1: Падающий тест версии**
В `tests/test_folder.py` строку 161 `self.assertEqual(agent.AGENT_VERSION, 9)` заменить на:
```python
self.assertEqual(agent.AGENT_VERSION, 10)
```
Run: `/usr/bin/python3 -m unittest tests.test_folder -v`
Expected: FAIL — `AssertionError: 9 != 10`.
- [ ] **Step 2: Версия**
В `src/agent.py` строку `AGENT_VERSION = 9` заменить на `AGENT_VERSION = 10`.
Run: `/usr/bin/python3 -m unittest tests.test_folder -v`
Expected: PASS.
- [ ] **Step 3: README**
В `README.md` найти пункт 4, начинающийся с `4. **Особенности \`refresh\`.**`, и в конец этого пункта (после предложения
про `result.missing`) дописать новую строку того же отступа:
```markdown
С v10 у каждой задачи состояния есть `comments` — все комментарии задачи (автор — отображаемое имя, иначе логин; время
и текст — как в Jira). Они приходят тем же `search`; если Jira отдала не все, агент дочитывает их через
`/issue/{key}/comment`.
```
- [ ] **Step 4: CLAUDE.md**
В `CLAUDE.md`, раздел «Первая подсистема», заменить строку, начинающуюся с `Последний change —`, на:
```markdown
Последний change — `../jira-hub/openspec/changes/issue-card-modal/` (агент v10: комментарии в состоянии; решения агента — `openspec/changes/agent-v10-comments/design.md`), план — `docs/superpowers/plans/2026-10-15-agent-v10-comments.md`.
```
- [ ] **Step 5: Change решений агента**
Run: `openspec new change agent-v10-comments`
Expected: создан каталог `openspec/changes/agent-v10-comments/`.
Создать `openspec/changes/agent-v10-comments/design.md`:
```markdown
# agent-v10-comments — решения агента
Спека и дизайн изменения — в `jira-hub/openspec/changes/issue-card-modal/` (issue aep/jira-hub#37, aep/jira-agent#27).
Агентская часть — решения 1–4 дизайна хаба (поле `comment` в `search`, дочитывание, форма комментария, версия и выкатка)
и требование `agent` «Чтение состояния».
Решения владельца 15.10.2026 (brainstorming jira-hub#37): комментарии задачи нужны в карточке веб-приложения; два change
по репозиториям, сначала агент; автор — отображаемое имя, как в Jira над комментарием.
## Decisions made without discussion
Рулинги R-1…R-5 записаны в `jira-hub/openspec/changes/issue-card-modal/design.md`
(раздел «Decisions made without discussion»); здесь — только решения, принятые при исполнении агентской части.
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
```
(Таблица пока без строк; решения при исполнении дописываются строками `| R-6 | … |` и далее.)
- [ ] **Step 6: Весь набор**
Run: `/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .`
Expected: `OK`.
- [ ] **Step 7: Commit**
```bash
git add src/agent.py tests/test_folder.py README.md CLAUDE.md openspec/changes/agent-v10-comments
git commit -m "chore(agent): версия 10, README и решения агента для комментариев (#27)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
## После плана
- Финальное ревью ветки (opus), затем PR в aep/jira-agent со ссылкой на #27 и на change `issue-card-modal`.
- После merge: `uv run --project ../jira-hub jira-hub agent publish src/agent.py` из основного checkout `jira-agent` на
main — пишет в каталог живого хаба, выполняется только по отдельной команде владельца; проверка — MCP `agent_status`
«обновление: ok v9 → v10».