From bd8c769e756a59edbad2e521cc0986c374d6c215 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 14 Oct 2026 12:50:44 +0400 Subject: [PATCH] =?UTF-8?q?feat(queue,mcp):=20=D0=BE=D1=82=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B0=D0=B5=D1=82=20=D0=BE=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B5=20=D1=83=20=D0=B0=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0;=20=D0=BF=D0=BE=D0=B4=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=BA=D0=B0=20=D0=BF=D1=80=D0=BE=20labels=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cancel_batch возвращает still_claimed — команды пакета в статусе claimed, которые отмена не останавливает. MCP batch_cancel выводит строку на каждую такую команду с подсказкой про POST /commands/N/cancel с force. Описание batch_create предупреждает, что ensure_issue нужны labels с базовой меткой AWPCA, иначе задача выпадет из базовой JQL и из зеркала. M-11 (hub#12), T3.7 (agent#4). Co-Authored-By: Claude Opus 5.5 --- src/jira_hub/mcp_server.py | 10 ++++++++-- src/jira_hub/queue.py | 5 ++++- tests/test_mcp.py | 13 +++++++++++++ tests/test_queue.py | 9 +++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/jira_hub/mcp_server.py b/src/jira_hub/mcp_server.py index 3e66cb4..bcf9a6d 100644 --- a/src/jira_hub/mcp_server.py +++ b/src/jira_hub/mcp_server.py @@ -91,6 +91,9 @@ def fmt_batch(b) -> str: for c in b.get("commands", []): lines.append(f" {c['seq']}. #{c['id']} {c['type']} {json.dumps(c['params'], ensure_ascii=False)} → {c['status']}" + (f" ({c['error']})" if c.get("error") else "") + (f" {json.dumps(c['result'], ensure_ascii=False)}" if c.get("result") else "")) + for s in b.get("still_claimed") or []: + lines.append(f" команда #{s['id']} уже у агента и будет выполнена; остановить — " + f"POST /commands/{s['id']}/cancel с force: true") return "\n".join(lines) @@ -142,7 +145,9 @@ def notes_set(key: str, fact_status: str | None = None, work_kind: str | None = @guard def batch_create(title: str, commands: list[dict]) -> str: """Поставить пакет команд. Каждая команда — {"type": ensure_issue|ensure_fields|ensure_comment|ensure_status| - ensure_link|ensure_sprint|refresh, "params": {...}} по контракту specs/commands. Хвостовой refresh хаб добавит сам.""" + ensure_link|ensure_sprint|refresh, "params": {...}} по контракту specs/commands. Хвостовой refresh хаб добавит сам. + ensure_issue передавайте с labels, включающими базовую метку проекта (AWPCA): хаб и агент метки сами не добавляют, + задача без неё выпадет из базовой JQL и из зеркала.""" b = _client().post("/batches", {"title": title, "author": AUTHOR, "commands": commands}) return f"пакет #{b['id']} поставлен: {b['count']} команд, оценка ≈ {round(b['estimate_seconds'] / 60)} мин" @@ -186,7 +191,8 @@ def command_retry(id: int) -> str: @mcp.tool() @guard def batch_cancel(id: int) -> str: - """Отменить пакет (только команды в очереди; взятую агентом — command_cancel с force через API).""" + """Отменить пакет: команды в очереди отменяются; взятую агентом команду отмена не останавливает — + в ответе подсказка (POST /commands/N/cancel с force).""" return fmt_batch(_client().post(f"/batches/{id}/cancel", {})) diff --git a/src/jira_hub/queue.py b/src/jira_hub/queue.py index b392f84..135ac59 100644 --- a/src/jira_hub/queue.py +++ b/src/jira_hub/queue.py @@ -287,7 +287,10 @@ def cancel_batch(conn, batch_id) -> dict: now = db.now() conn.execute("UPDATE batch SET cancelled_at = ? WHERE id = ? AND cancelled_at IS NULL", (now, batch_id)) conn.execute("UPDATE command SET status = 'cancelled', finished_at = ? WHERE batch_id = ? AND status = 'queued'", (now, batch_id)) - return batch_view(conn, batch_id) + view = batch_view(conn, batch_id) + # взятые агентом команды отмена пакета не останавливает: они будут выполнены (M-11 #12) + view["still_claimed"] = [{"id": c["id"], "type": c["type"]} for c in view["commands"] if c["status"] == "claimed"] + return view def cancel_command(conn, command_id, force=False) -> dict: diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 6c460f8..c7af1ef 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -189,3 +189,16 @@ def test_batch_wait_does_not_block_event_loop(monkeypatch): out, ticks = asyncio.run(scenario()) assert "done" in out and len(ticks) == 5 + + +def test_fmt_batch_warns_about_claimed(): + b = {"id": 9, "title": "x", "author": "claude", "created_at": "2026-10-14T10:00:00+00:00", "status": "running", + "remaining_seconds": 120, "counts": {"claimed": 1, "cancelled": 1}, "commands": [], + "still_claimed": [{"id": 30, "type": "ensure_fields"}]} + text = mcp_server.fmt_batch(b) + assert "команда #30 уже у агента и будет выполнена" in text and "POST /commands/30/cancel" in text + + +def test_batch_create_mentions_labels(): + doc = mcp_server.batch_create.__doc__ or "" + assert "labels" in doc and "AWPCA" in doc diff --git a/tests/test_queue.py b/tests/test_queue.py index 1ddd01e..bd91ca6 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -456,3 +456,12 @@ def test_refresh_failed_by_limit_finishes_sync_run(): queue.claim(conn) assert queue.get_command(conn, r["id"])["status"] == "failed" assert conn.execute("SELECT finished_at FROM sync_run WHERE command_id = ?", (r["id"],)).fetchone()["finished_at"] + + +def test_cancel_batch_reports_still_claimed(): + conn = fresh() + b = queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2")]) + c = queue.claim(conn) + view = queue.cancel_batch(conn, b["id"]) + assert view["still_claimed"] == [{"id": c["id"], "type": "ensure_fields"}] + assert queue.get_command(conn, c["id"])["status"] == "claimed"