19 KiB
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, только если в ответеsearchlen(comment.comments) < comment.total. commentsесть у каждой задачи состояния, в том числе у соседей; нет комментариев —[].- Коммиты на русском, трейлер
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
Review Focus
- Ответ
searchбезfields.commentвовсе (поле недоступно по правам) —comments = [],refreshне падает (Task 1, тестtest_comments_field_missing). comment.totalотсутствует илиnull— считать полным полученный список, без дочитывания (Task 1, тот же тест).- Автор комментария удалён из Jira (
author=null) —author = None, текст на месте (Task 1,test_comments_in_state). - Комментарий с пустым или отсутствующим
body—"", неNone(Task 1,test_comments_in_state). - Дочитывание вернуло 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 добавить:
self.search_comment_cap = None # сколько комментариев задачи отдаёт search (None — все), как обрезка Jira
Добавить метод после add:
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 целиком:
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, заменить строку возврата:
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 (создание комментария) заменить две строки
self.comment_seq += 1
lst.append({"id": str(self.comment_seq), "body": body["body"]})
на
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 добавить:
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:
STATE_FIELDS = ["summary", "description", "status", "priority", "assignee", "reporter", "duedate", "labels",
"issuetype", "created", "updated", "issuelinks", "comment"]
После метода links_from добавить:
@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"]) добавить:
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
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) заменить на:
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`) дописать новую строку того же отступа:
С v10 у каждой задачи состояния есть `comments` — все комментарии задачи (автор — отображаемое имя, иначе логин; время
и текст — как в Jira). Они приходят тем же `search`; если Jira отдала не все, агент дочитывает их через
`/issue/{key}/comment`.
- Step 4: CLAUDE.md
В CLAUDE.md, раздел «Первая подсистема», заменить строку, начинающуюся с Последний change —, на:
Последний 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:
# 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
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из основного checkoutjira-agentна main — пишет в каталог живого хаба, выполняется только по отдельной команде владельца; проверка — MCPagent_status«обновление: ok v9 → v10».