37 KiB
projects — агент v12: доски проектов из параметров команд — план
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: агент v12 берёт доски Jira из параметров команд: ensure_sprint — из board_id команды, refresh — из
boards ([{id, type}]); для досок с type = null определяет тип (GET /rest/agile/1.0/board/{id}) и возвращает его
в результате boards, спринты задач собирает со всех scrum-досок; без board_id/boards работает по
settings.board_id, как v11.
Architecture: всё в src/agent.py, класс Executor. Кэш спринтов _sprints из одного словаря становится словарём
«доска → (имя → спринт)» и по-прежнему сбрасывается в Executor.run на каждую команду (а при смене настроек Runner
пересоздаёт Executor). sprints(board) читает активные и будущие спринты одной доски; do_ensure_sprint берёт доску
p["board_id"], иначе settings.board_id. В do_refresh первым шагом refresh_boards(p) определяет типы досок с
type = null (board_type(board), ошибка HTTP — None и строка в логе) и отдаёт список scrum-досок и словарь для
результата; sprint_names(keys, boards) обходит доски по порядку, последнее найденное имя побеждает.
Tech Stack: Python 3.9.6 (/usr/bin/python3), только стандартная библиотека; тесты — unittest на фейках
tests/fake_jira.py, tests/fake_hub.py.
Spec: change хаба projects (aep/jira-hub#61), пока в worktree хаба:
/Users/andrey.epifancev/projects/jira-bridge/jira-hub/.wt/scopes/openspec/changes/projects/ —
specs/agent/spec.md (ADDED «Доски проектов» — основное, MODIFIED «Состав и настройки агента»),
specs/commands/spec.md (таблица команд: ensure_sprint.board_id, refresh.boards, результат boards),
design.md (решения 4, 7, 14, 15; «Decisions made without discussion» № 6, 7, 14). Действующая спека агента —
/Users/andrey.epifancev/projects/jira-bridge/jira-hub/.wt/scopes/openspec/specs/agent/spec.md. Решения агентской
части — openspec/changes/agent-v12-project-boards/design.md (создаётся в Task 3).
Worktree: /Users/andrey.epifancev/projects/jira-bridge/jira-agent/.wt/projects, ветка feat/35-projects
(aep/jira-agent#35). Все команды — из корня worktree; основные checkout'ы jira-agent и jira-hub не трогать.
Global Constraints
- Python ≥ 3.9 (на виртуалке 3.9.6, тесты —
/usr/bin/python3), только стандартная библиотека; синтаксис 3.10+ (X | Yв аннотациях функций,match) запрещён —tests/test_py39.py. Для кортежей в аннотациях —typing.Tuple. - Тесты:
/usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .— зелёным считается только весь набор (до плана — 262 теста; набор идёт ≈2 минуты, прогон всего набора — в фоне). AGENT_VERSIONSHALL быть 12.ensure_sprintSHALL брать доску изboard_idкоманды; нетboard_id(или онnull) — изsettings.board_id, как v11.refreshSHALL брать доски изboards([{id, type}],type—"scrum","kanban"илиnull). Нет ключаboards(хаб до проектов) — одна доскаsettings.board_id, как v11, результат безboards.- Для каждой доски с
type = null— ровно одинGET /rest/agile/1.0/board/{id}; тип из ответа (type) —"scrum"или"kanban", иначеnull; ошибка запроса —null, строка в логе с номером доски и кодом ответа,refreshпродолжается. - Результат
refresh—boards: {"<id>": "scrum" | "kanban" | null}только для досок, чей тип агент запрашивал; ключ — строка. - Имена спринтов — со всех досок типа
scrum(известного или только что определённого) в порядкеboards;kanbanиnullпропускаются; задача в спринтах двух досок получает последнее найденное имя (как в v11). ensure_sprintна доске без спринтов (Jira ответила ошибкой на список спринтов) —failedс текстом Jira.- Коммиты на русском с
(#35), трейлерCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
Review Focus
- Хаб с проектами прислал
boards: [](ни у одного проекта нет доски), а в его настройках осталсяboard_id = 4216(решение 14 хаба) — агент не читает спринты доски из настроек, спринты задач —null(Task 2,test_empty_boards_no_settings_fallback). GET /board/{id}ответил 5xx (а не 404) — типnull, строка в логе с кодом,refreshзавершаетсяdone, состояние уходит в хаб (Task 2,test_board_type_5xx_logged_refresh_continues).- Одна и та же доска с
type = nullдважды вboards— один запрос типа, один ключ в результате (Task 2,test_duplicate_null_board_requested_once). refresh {keys: []}с доской неизвестного типа — тип всё равно определяется и возвращается (иначе хвост пакета без задач оставил бы тип неизвестным) (Task 2,test_empty_keys_still_resolves_boards).- Доска, известная хабу как
scrum, перестала отдавать спринты (стала канбаном, удалена) —refreshfailedс текстом Jira, как v11, а не молчаливое обнуление спринтов в зеркале (Task 2,test_scrum_board_sprints_error_fails_refresh; решение R-32).
Task 1: Спринты по доске и ensure_sprint с board_id из команды
Модель исполнителя: sonnet.
Files:
- Modify:
tests/fake_jira.py—FakeJira.__init__(новые поля), маршруты Agile вroute(стр. 352–380) - Modify:
src/agent.py—Executor.__init__иExecutor.run(стр. 556–564, сброс_sprints),Executor.sprints(стр. 838–851),Executor.do_ensure_sprint(стр. 853–873) - Test:
tests/test_executor_links.py
Interfaces:
-
Consumes: —
-
Produces:
FakeJira.boards: Dict[int, str]— доска → тип, по умолчанию{4216: "scrum"}.GET /rest/agile/1.0/board/{id}: есть — 200{"id": id, "name": "Доска <id>", "type": <тип>}, нет — 404{"errorMessages": ["Board does not exist"]}.GET /rest/agile/1.0/board/{id}/sprintу доски с типом"kanban"— 400{"errorMessages": ["The board does not support sprints"]}.FakeJira.board_sprints: Dict[int, List[dict]]— спринты доски; доски нет в словаре — общийself.sprints(как раньше, чтобы старые тесты не менялись).FakeJira.sprint_members: Dict[int, List[str]]— id спринта → ключи задач; для такого спринтаGET /sprint/{id}/issueотдаёт эти ключи (с фильтром поjqlkey in (…)) вместо сравнения по имени.FakeJira.all_sprints() -> List[dict]—self.sprints+ все спискиboard_sprints; по нему ищут спринт/sprint/{id}/issueиGET /rest/agile/1.0/issue/{key}.Executor._sprints: Dict[str, Dict[str, dict]]—str(доска)→ имя → спринт;{}в__init__и в началеrun.Executor.sprints(self, board: Any) -> Dict[str, dict]— активные и будущие спринты доскиboard(постранично, как v11), с кэшем на команду; ошибку Jira не глотает.
-
Step 1: Фейк — доски, спринты по доске, участники спринта
Добавить в FakeJira поля и маршрут GET /rest/agile/1.0/board/{id} из Interfaces (регулярка ^/rest/agile/1\.0/board/(\d+)$
не пересекается с …/sprint), 400 для канбан-доски в маршруте /board/{id}/sprint, список спринтов по board_sprints,
all_sprints() в маршрутах /sprint/{id}/issue и /agile/1.0/issue/{key}, sprint_members в GET /sprint/{id}/issue.
Run: /usr/bin/python3 -m unittest tests.test_executor_links tests.test_refresh -v
Expected: PASS (фейк по умолчанию ведёт себя как раньше).
- Step 2: Падающие тесты
В tests/test_executor_links.py, после test_sprint_without_board:
def test_sprint_board_from_params(self):
self.fake.boards[5120] = "scrum"
self.fake.board_sprints[5120] = [{"id": 7, "name": "Спринт K", "state": "active"}]
out = self.run_cmd("ensure_sprint", key="AURORA-1", sprint="Спринт K", board_id=5120)
self.assertEqual(out["status"], "done", out)
self.assertEqual(out["result"], {"sprint": "Спринт K", "from": None})
self.assertEqual(self.fake.issues["AURORA-1"]["sprint"], "Спринт K")
paths = [r[1] for r in self.fake.requests]
self.assertIn("/rest/agile/1.0/board/5120/sprint", paths)
self.assertNotIn("/rest/agile/1.0/board/4216/sprint", paths) # settings.board_id = 4216 не трогается
again = self.run_cmd("ensure_sprint", key="AURORA-1", sprint="Спринт K", board_id=5120)
self.assertEqual(again["result"], {"sprint": "Спринт K", "changed": False})
def test_sprint_board_from_params_without_settings(self): # сценарий «Спринт по доске из команды»
self.ex.s["board_id"] = None
out = self.run_cmd("ensure_sprint", key="AURORA-1", sprint="Спринт 2", board_id=4216)
self.assertEqual(out["status"], "done", out)
self.assertEqual(self.fake.issues["AURORA-1"]["sprint"], "Спринт 2")
def test_sprint_unknown_name_names_param_board(self):
self.fake.board_sprints[5120] = [{"id": 7, "name": "Спринт K", "state": "active"}]
out = self.run_cmd("ensure_sprint", key="AURORA-1", sprint="Спринт 2", board_id=5120)
self.assertEqual(out["status"], "failed")
self.assertIn("5120", out["error"])
self.assertIn("Спринт K", out["error"])
def test_sprint_kanban_board_failed_with_jira_text(self):
self.fake.boards[5120] = "kanban"
out = self.run_cmd("ensure_sprint", key="AURORA-1", sprint="Спринт 2", board_id=5120)
self.assertEqual(out["status"], "failed")
self.assertIn("HTTP 400", out["error"])
self.assertIn("does not support sprints", out["error"])
self.assertFalse(out["retryable"])
self.assertIsNone(self.fake.issues["AURORA-1"]["sprint"])
def test_sprint_backlog_ignores_board(self):
self.fake.boards[5120] = "kanban"
self.fake.issues["AURORA-1"]["sprint"] = "Спринт 1"
out = self.run_cmd("ensure_sprint", key="AURORA-1", sprint="backlog", board_id=5120)
self.assertEqual(out["result"], {"sprint": None, "from": "Спринт 1"})
self.assertFalse(any("/board/" in r[1] for r in self.fake.requests))
test_sprint_without_board (без board_id в команде и в настройках) не меняется: ошибка по-прежнему содержит board_id.
Run: /usr/bin/python3 -m unittest tests.test_executor_links -v
Expected: FAIL test_sprint_board_from_params (идёт на доску 4216: «нет спринта «Спринт K»»),
test_sprint_board_from_params_without_settings («board_id не задан…»), test_sprint_unknown_name_names_param_board
(в тексте 4216, а не 5120); test_sprint_kanban_board_failed_with_jira_text и test_sprint_backlog_ignores_board могут
пройти уже сейчас — это допустимо, они фиксируют поведение.
-
Step 3: Реализация
-
Executor._sprints = {}в__init__и в началеrun(вместоNone). -
sprints(self, board: Any) -> Dict[str, dict]— тело v11, доска из аргумента, кэшself._sprints[str(board)]; docstring: «Имя → спринт (active и future) доски board». -
do_ensure_sprint:board = p.get("board_id") or self.s.get("board_id")после веткиbacklog; нет доски —failed«board_id нет ни в команде, ни в настройках хаба — спринты недоступны»; дальше как v11 сself.sprints(board), в тексте «нет спринта …» — номерboard. -
sprint_namesпока зовётself.sprints(self.s["board_id"])(переделывается в Task 2).
Run: /usr/bin/python3 -m unittest tests.test_executor_links tests.test_refresh -v
Expected: PASS.
- Step 4: Весь набор
Run: /usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .
Expected: OK.
- Step 5: Commit
git add tests/fake_jira.py tests/test_executor_links.py src/agent.py
git commit -m "feat(agent): ensure_sprint по доске из команды, кэш спринтов по доскам (#35)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 2: refresh с досками проектов: тип доски, спринты со всех scrum-досок, результат boards
Модель исполнителя: sonnet.
Files:
- Modify:
src/agent.py— импортTupleизtyping(стр. 34), новыеExecutor.board_type,Executor.refresh_boards(разделrefresh, передsprint_names),Executor.sprint_names(стр. 1024–1043),Executor.do_refresh(стр. 1045–1095) - Test:
tests/test_refresh.py
Interfaces:
-
Consumes: Task 1 —
Executor.sprints(board),FakeJira.boards,FakeJira.board_sprints,FakeJira.sprint_members. -
Produces:
Executor.board_type(self, board: Any) -> Optional[str]—GET /rest/agile/1.0/board/{board};typeиз ответа, если он"scrum"или"kanban"(точное совпадение), иначеNoneи в лог"refresh: доска %s — тип «%s» не scrum/kanban" % (board, t);HttpErrorлюбого кода —Noneи в лог"refresh: доска %s — тип не определён (HTTP %d)" % (board, e.code);ConnectionErrorне ловит.Executor.refresh_boards(self, p: dict) -> Tuple[List[Any], Optional[Dict[str, Optional[str]]]]— (scrum-доски в порядкеboardsбез повторов поstr(id), словарь для результата). Нет ключаboards—([settings.board_id]или[],None). Есть — для каждой доски сtype is Noneодинboard_type; словарь — запрошенные доски (str(id)→ тип), пустой словарь возвращается какNone. Элемент безidпропускается.Executor.sprint_names(self, keys: List[str], boards: List[Any]) -> Dict[str, str]— как v11, но по каждой доскеboardsпо порядку (for board in boards: for name, sp in self.sprints(board).items(): …); пустыеkeysилиboards—{}.do_refresh: первым действиемscrum, types = self.refresh_boards(p)(до поиска и до раннего выхода по пустымkeys);names = self.sprint_names(keys, scrum); еслиtypes is not None—result["boards"] = types(и в раннем выходе по пустымkeys).
-
Step 1: Падающие тесты
В tests/test_refresh.py поменять вызов в test_sprint_names_paginated на
self.ex.sprint_names(["AURORA-%d" % i for i in range(1, 101)], [4216]) и добавить:
JQL = "project = AURORA AND labels in (AWPCA, STOCKS_DRRK)"
def refresh(self, **params):
return self.ex.run({"id": 5, "type": "refresh", "params": params})
def logged(self, fn):
logs, orig = [], agent.log
agent.log = logs.append
try:
return fn(), logs
finally:
agent.log = orig
def test_two_boards_one_new(self): # сценарий «Две доски, одна новая»
self.fake.boards[5120] = "kanban"
self.fake.add("AURORA-1", sprint="Спринт 2")
out = self.refresh(jql=self.JQL, boards=[{"id": 4216, "type": "scrum"}, {"id": 5120, "type": None}])
self.assertEqual(out["status"], "done", out)
self.assertEqual(out["result"]["boards"], {"5120": "kanban"})
paths = [r[1] for r in self.fake.requests]
self.assertEqual(paths.count("/rest/agile/1.0/board/5120"), 1)
self.assertNotIn("/rest/agile/1.0/board/5120/sprint", paths)
self.assertNotIn("/rest/agile/1.0/board/4216", paths) # тип известен — не спрашиваем
self.assertEqual(self.hub.states[0]["issues"][0]["sprint"], "Спринт 2")
def test_board_not_found(self): # сценарий «Доска не найдена»
self.fake.add("AURORA-1")
out, logs = self.logged(lambda: self.refresh(jql=self.JQL, boards=[{"id": 9999, "type": None}]))
self.assertEqual(out["status"], "done", out)
self.assertEqual(out["result"]["boards"], {"9999": None})
self.assertTrue(any("9999" in l and "HTTP 404" in l for l in logs), logs)
self.assertEqual(len(self.hub.states), 1)
def test_new_scrum_board_sprints_read(self):
self.fake.boards[5120] = "scrum"
self.fake.board_sprints[5120] = [{"id": 7, "name": "Спринт K", "state": "active"}]
self.fake.add("AURORA-1", sprint="Спринт K")
out = self.refresh(jql=self.JQL, boards=[{"id": 5120, "type": None}])
self.assertEqual(out["result"]["boards"], {"5120": "scrum"})
self.assertEqual(self.hub.states[0]["issues"][0]["sprint"], "Спринт K")
self.assertNotIn("/rest/agile/1.0/board/4216/sprint", [r[1] for r in self.fake.requests])
def test_old_hub_without_boards(self): # сценарий «Старый хаб»
self.fake.add("AURORA-1", sprint="Спринт 2")
out = self.refresh(jql=self.JQL)
self.assertNotIn("boards", out["result"])
self.assertIn("/rest/agile/1.0/board/4216/sprint", [r[1] for r in self.fake.requests])
self.assertEqual(self.hub.states[0]["issues"][0]["sprint"], "Спринт 2")
def test_known_boards_no_type_requests(self):
self.fake.add("AURORA-1", sprint="Спринт 2")
out = self.refresh(jql=self.JQL, boards=[{"id": 4216, "type": "scrum"}])
self.assertNotIn("boards", out["result"])
self.assertNotIn("/rest/agile/1.0/board/4216", [r[1] for r in self.fake.requests])
self.assertEqual(self.hub.states[0]["issues"][0]["sprint"], "Спринт 2")
def test_empty_boards_no_settings_fallback(self):
self.fake.add("AURORA-1", sprint="Спринт 2")
out = self.refresh(jql=self.JQL, boards=[])
self.assertEqual(out["status"], "done", out)
self.assertNotIn("boards", out["result"])
self.assertFalse(any("/rest/agile/1.0/board/" in r[1] for r in self.fake.requests))
self.assertIsNone(self.hub.states[0]["issues"][0]["sprint"])
def test_board_type_5xx_logged_refresh_continues(self):
self.fake.add("AURORA-1")
self.fake.fail_next.append((500, {})) # первый вызов Jira в refresh — тип доски
out, logs = self.logged(lambda: self.refresh(jql=self.JQL, boards=[{"id": 5120, "type": None}]))
self.assertEqual(out["status"], "done", out)
self.assertEqual(out["result"]["boards"], {"5120": None})
self.assertTrue(any("5120" in l and "HTTP 500" in l for l in logs), logs)
def test_board_other_type_is_null(self):
self.fake.boards[7000] = "simple"
out, logs = self.logged(lambda: self.refresh(keys=[], boards=[{"id": 7000, "type": None}]))
self.assertEqual(out["result"]["boards"], {"7000": None})
self.assertTrue(any("7000" in l and "simple" in l for l in logs), logs)
def test_duplicate_null_board_requested_once(self):
self.fake.boards[5120] = "kanban"
out = self.refresh(keys=[], boards=[{"id": 5120, "type": None}, {"id": 5120, "type": None}])
self.assertEqual(out["result"]["boards"], {"5120": "kanban"})
self.assertEqual([r[1] for r in self.fake.requests].count("/rest/agile/1.0/board/5120"), 1)
def test_empty_keys_still_resolves_boards(self):
self.fake.boards[5120] = "kanban"
out = self.refresh(keys=[], boards=[{"id": 5120, "type": None}])
self.assertEqual(out["status"], "done")
self.assertEqual(out["result"], {"issues": 0, "neighbors": 0, "pages": 0, "boards": {"5120": "kanban"}})
def test_last_found_sprint_wins_across_boards(self):
self.fake.boards[5120] = "scrum"
self.fake.board_sprints[5120] = [{"id": 7, "name": "Спринт K", "state": "active"}]
self.fake.add("AURORA-1")
self.fake.sprint_members = {2: ["AURORA-1"], 7: ["AURORA-1"]}
both = [{"id": 4216, "type": "scrum"}, {"id": 5120, "type": "scrum"}]
self.refresh(jql=self.JQL, boards=both)
self.assertEqual(self.hub.states[-1]["issues"][0]["sprint"], "Спринт K")
self.refresh(jql=self.JQL, boards=both[::-1])
self.assertEqual(self.hub.states[-1]["issues"][0]["sprint"], "Спринт 2")
def test_scrum_board_sprints_error_fails_refresh(self): # R-32
self.fake.boards[5120] = "kanban" # хаб думает, что scrum
self.fake.add("AURORA-1")
out = self.refresh(jql=self.JQL, boards=[{"id": 5120, "type": "scrum"}])
self.assertEqual(out["status"], "failed")
self.assertIn("HTTP 400", out["error"])
self.assertEqual(self.hub.states, [])
FakeJira.search_keys на любую JQL без key in и без reporter отдаёт задачи с меткой AWPCA — self.JQL подходит.
В test_board_type_5xx_logged_refresh_continues 500 достаётся запросу типа доски потому, что refresh_boards — первый
вызов Jira в do_refresh (R-30).
Run: /usr/bin/python3 -m unittest tests.test_refresh -v
Expected: FAIL — KeyError: 'boards' в результате, TypeError у test_sprint_names_paginated (лишний аргумент) и
запросы к доске 4216 там, где их быть не должно; test_old_hub_without_boards проходит уже сейчас.
- Step 2: Реализация
board_type, refresh_boards, sprint_names(keys, boards) и правка do_refresh — по Interfaces. Комментарий R-6 у
sprint_names сохранить.
Run: /usr/bin/python3 -m unittest tests.test_refresh tests.test_executor_links -v
Expected: PASS.
- Step 3: Весь набор
Run: /usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .
Expected: OK.
- Step 4: Commit
git add src/agent.py tests/test_refresh.py
git commit -m "feat(agent): refresh по доскам проектов — тип доски, спринты со всех scrum-досок, результат boards (#35)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 3: Версия 12, документация, change решений агента
Модель исполнителя: haiku (готовый текст).
Files:
- Modify:
src/agent.py:36(AGENT_VERSION) - Modify:
tests/test_folder.py:185-186(test_version) - Modify:
README.md— абзац «Остальные настройки…» (стр. 27), строкиensure_sprintиrefreshтаблицы команд (стр. 173, 175), пункт «Версия» (стр. 193–200), пункт 3 «Известных ограничений» (стр. 237–238) - Modify:
ARCHITECTURE.md— строка «Исполнитель» таблицы разделов (стр. 14) - Modify:
CLAUDE.md— раздел «Первая подсистема», строка «Последний change —» - Create:
openspec/changes/agent-v12-project-boards/design.md(черезopenspec new change agent-v12-project-boards)
Interfaces:
-
Consumes: Task 1–2.
-
Produces:
AGENT_VERSION = 12. -
Step 1: Падающий тест версии
В tests/test_folder.py, test_version: assertEqual(agent.AGENT_VERSION, 11) → assertEqual(agent.AGENT_VERSION, 12).
Run: /usr/bin/python3 -m unittest tests.test_folder -v
Expected: FAIL — AssertionError: 11 != 12.
- Step 2: Версия
В src/agent.py AGENT_VERSION = 11 → AGENT_VERSION = 12.
Run: /usr/bin/python3 -m unittest tests.test_folder -v
Expected: PASS.
-
Step 3: README
-
Стр. 27:
(id полей, типы задач, карта переходов, типы связей, доска, темп)→(id полей, типы задач, карта переходов, типы связей, темп), а после предложения дописать: «Доски Jira (с v12) приходят в параметрах команд:ensure_sprint—board_id,refresh—boards;board_idиз настроек — запасной для команд старого хаба.» -
Строка
ensure_sprint:переносит в спринт доскиboard_idкоманды (без него — доски из настроек хаба; активные и будущие спринты) или в бэклог; доска без спринтов (канбан) —failedс текстом Jira. -
Строка
refresh: дописать в конец ячейки; доски с неизвестным типом (boards,type = null) — запрос/rest/agile/1.0/board/{id}, тип в результатеboards; спринты — со всех scrum-досок. -
Пункт «Версия»:
(сейчас 11)→(сейчас 12); после строки «Агент v10 наensure_attachment…» дописать строкой того же отступа:
Версия 12 — доски проектов из параметров команд (`ensure_sprint.board_id`, `refresh.boards`), тип доски в результате
`refresh`, спринты со всех scrum-досок; без этих параметров — `board_id` из настроек, как v11. Хаб с проектами
выкатывается только после v12 (design хаба `projects`, решение 14).
-
Пункт 3 ограничений:
**Спринт вrefresh— только активные и будущие спринты доски.**→**Спринт вrefresh— только активные и будущие спринты scrum-досок.**; после «У задач закрытых спринтовsprint = null.» дописать: «Задачи канбан-досок и досок неизвестного типа — тожеnull. Задача в спринтах двух досок получает имя спринта последней по спискуboards. Если scrum-доска не отдала спринты (удалена, стала канбаном),refreshзавершаетсяfailedс текстом Jira (R-32).» -
Step 4: ARCHITECTURE.md и CLAUDE.md
-
ARCHITECTURE.md, строка «Исполнитель», колонка «Ответственность» — дописать в конец:; доски — из параметров команд (ensure_sprint.board_id,refresh.boards, тип доски —board_type),board_idнастроек — запасной. -
CLAUDE.md, строкуПоследний change — …заменить на:
Последний change — `../jira-hub/openspec/changes/projects/` (агент v12: доски проектов из параметров команд, тип доски в `refresh`; решения агента — `openspec/changes/agent-v12-project-boards/design.md`), план — `docs/superpowers/plans/2026-10-19-projects-agent.md`.
- Step 5: Change решений агента
Run: openspec new change agent-v12-project-boards
Expected: создан каталог openspec/changes/agent-v12-project-boards/.
Создать openspec/changes/agent-v12-project-boards/design.md; таблица — те же строки R-30…R-35, что в разделе
«Decisions made without discussion» этого плана; решения, принятые при исполнении, контроллер дописывает следующими
номерами, начиная с R-36:
# agent-v12-project-boards — решения агента
Спека и дизайн изменения — в `jira-hub/openspec/changes/projects/` (issue aep/jira-hub#61, aep/jira-agent#35).
Агентская часть — решения 7, 14, 15 дизайна хаба и требования `agent` «Доски проектов» (ADDED) и «Состав и настройки
агента» (MODIFIED); контракт команд — `commands` (`ensure_sprint.board_id`, `refresh.boards`, результат `boards`).
## Decisions made without discussion
Решения 1–21 записаны в `jira-hub/openspec/changes/projects/design.md` (раздел «Decisions made without discussion»;
для агента — № 6, 7, 14); здесь — только решения агентской части.
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
<строки R-30…R-35 из плана>
- Step 6: Весь набор и синтаксис 3.9
Run: /usr/bin/python3 -W error::ResourceWarning -m unittest discover -s tests -t .
Expected: OK (262 + новые из Task 1–2).
Run: /usr/bin/python3 -m unittest tests.test_py39 -v
Expected: PASS.
- Step 7: Commit
git add src/agent.py tests/test_folder.py README.md ARCHITECTURE.md CLAUDE.md openspec/changes/agent-v12-project-boards
git commit -m "chore(agent): версия 12, README и решения агента для досок проектов (#35)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Decisions made without discussion
Нумерация рулингов агентской части — с R-30, чтобы не пересечься с рулингами исполнения хабовой части change.
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| R-30 | 2026-10-19 | Типы досок определяются первым шагом refresh — до поиска и в том числе при пустых keys |
Спека: «для каждой доски с type = null агент SHALL запросить» — без условий; хвост пакета refresh {keys: []} не должен оставлять тип неизвестным |
Лишний GET /board/{id} на доску неизвестного типа, если поиск затем упадёт; результат с типом при этом теряется до следующего refresh |
| R-31 | 2026-10-19 | Любой HttpError на GET /board/{id} (в том числе 5xx) — тип null и строка в логе, refresh продолжается; ConnectionError не ловится — весь refresh failed с повтором, как при обрыве связи с Jira |
Спека: «ошибка запроса — в лог, refresh продолжается»; null тип в хабе не меняет, следующий refresh спросит снова; обрыв связи — не свойство доски |
Временный 5xx откладывает определение типа до следующего refresh (автообновление) |
| R-32 | 2026-10-19 | Ошибка чтения спринтов scrum-доски (известной или только что определённой) в refresh не глотается: refresh failed с текстом Jira, как v11 |
Пропуск доски записал бы в зеркало sprint = null всем её задачам; спека о такой ошибке молчит |
Одна сломанная доска (удалена, стала канбаном) останавливает обновление зеркала всех проектов, пока владелец не поправит доску в базе хаба |
| R-33 | 2026-10-19 | Запасной settings.board_id — только когда ключа boards нет (refresh) или board_id нет / null (ensure_sprint); boards: [] — ни одной доски, спринты не читаются |
Новый хаб оставляет board_id в настройках (решение 14 хаба) и шлёт boards всегда; [] от него значит «у проектов нет досок», а не «старый хаб» |
Если старый хаб когда-нибудь пришлёт boards: [], спринты пропадут из зеркала; такого хаба нет |
| R-34 | 2026-10-19 | Ключ boards в результате refresh — только если агент запросил тип хотя бы одной доски; иначе ключа нет |
Спека: результат «для досок, чей тип агент запрашивал»; сценарий «Старый хаб» — «результат без boards» |
Хаб SHALL принимать результат без boards; если он ждёт {}, — ошибка разбора результата в хабе |
| R-35 | 2026-10-19 | Доски сравниваются и выводятся по str(id); повторы в boards отбрасываются (запрос типа — один); элемент без id пропускается; тип принимается только точным "scrum"/"kanban", иное ("simple" и т. п.) — null со строкой в логе; спринты читаются в порядке boards, последнее найденное имя побеждает |
Ключи результата в спеке — строки; хаб шлёт без повторов, но повтор не должен стоить второго запроса; сценарий спеки — «один раз» | Доска "4216" строкой и 4216 числом считаются одной; другой регистр типа от Jira ("Scrum") дал бы null |
После плана
- Финальное ревью ветки (opus), затем PR в aep/jira-agent со ссылкой на #35 и на change
projectsхаба. Агент сливается и публикуется первым (design хаба, решения 14 и 15). - После merge:
uv run --project ../jira-hub jira-hub agent publish src/agent.pyиз основного checkoutjira-agentна main — пишет в каталог живого хаба, выполняется только по отдельной команде владельца; проверка — MCPagent_status«обновление: ok v11 → v12». Затем — выкатка хаба (шаги 2–4 решения 14 хаба).