From e968617efb858304786d2b79159f490c0dbf4bc8 Mon Sep 17 00:00:00 2001 From: AI Agent Date: Mon, 25 May 2026 17:31:17 -0700 Subject: [PATCH] fix: surface attempt state in MCP work guidance --- app/main.py | 33 +++++++++++++--- docs/agent-guide.md | 13 +++++++ tests/test_api_mcp.py | 90 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/app/main.py b/app/main.py index 034b80f0..41fc2e26 100644 --- a/app/main.py +++ b/app/main.py @@ -1601,6 +1601,13 @@ def work_proof_guidance(bounty: Bounty) -> str: def work_proof_guidance_json(bounty: Bounty) -> dict[str, Any]: bounty_data = bounty_to_dict(bounty) + now = _utc_now() + attempts = session.scalars( + select(BountyAttempt) + .where(*_active_attempt_conditions(bounty.id, now)) + .order_by(BountyAttempt.created_at.desc(), BountyAttempt.id.desc()) + .limit(10) + ).all() return { "bounty_id": bounty_data["id"], "issue_number": bounty_data["issue_number"], @@ -1614,10 +1621,21 @@ def work_proof_guidance_json(bounty: Bounty) -> dict[str, Any]: "issue_url": bounty_data["issue_url"], "title": bounty_data["title"], "acceptance": bounty_data["acceptance"], + "active_attempts": [bounty_attempt_to_dict(attempt, now) for attempt in attempts], + "attempt_warnings": bounty_attempt_warnings(session, bounty, now), + "attempt_registration": { + "method": "POST", + "path": f"/api/v1/bounties/{bounty.id}/attempts", + "ttl_seconds_default": DEFAULT_ATTEMPT_TTL_SECONDS, + "ttl_seconds_min": MIN_ATTEMPT_TTL_SECONDS, + "ttl_seconds_max": MAX_ATTEMPT_TTL_SECONDS, + "advisory_only": True, + }, "submission_format": ( - "Open a focused PR or issue that links this bounty, include specific " - "test or behavior evidence, then comment /claim with the PR or " - "evidence URL and verification summary." + "Before opening a PR, register an advisory attempt if the bounty is " + "open and has award slots. Then open a focused PR or issue that links " + "this bounty, include specific test or behavior evidence, and comment " + "/claim with the PR or evidence URL and verification summary." ), "safety_rules": [ "Do not include private keys, seed material, secrets, deployment " @@ -1635,9 +1653,14 @@ def generic_work_proof_guidance_json() -> dict[str, Any]: "repository": None, "issue_url": None, "acceptance": None, + "active_attempts": [], + "attempt_warnings": ["select a bounty before inspecting or registering attempts"], + "attempt_registration": None, "submission_format": ( - "Open a focused PR or issue, reference the MRWK bounty, include test " - "evidence, and wait for a maintainer to apply mrwk:accepted." + "Select a concrete MRWK bounty first. For open bounties with award " + "slots, register an advisory attempt before opening a focused PR or " + "issue, reference the bounty, include test evidence, and wait for a " + "maintainer to apply mrwk:accepted." ), "safety_rules": [ "Do not include private keys, seed material, secrets, deployment " diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 4ce16574..ea274de7 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -162,6 +162,19 @@ curl -s -X POST "$MCP_HOST/mcp" \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_proof","arguments":{"hash":""}}}' ``` +Get machine-readable submission guidance for a concrete bounty before opening work: + +```bash +curl -s -X POST "$MCP_HOST/mcp" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"submit_work_proof","arguments":{"bounty_id":,"format":"json"}}}' +``` + +The structured response includes current `active_attempts`, `attempt_warnings`, +and the advisory attempt-registration path so agents can see overlap before +opening a PR. Generic guidance without a selected bounty does not include live +attempt state; select a bounty first. + Tools: - `list_bounties` diff --git a/tests/test_api_mcp.py b/tests/test_api_mcp.py index 20ac0411..ebb8efcb 100644 --- a/tests/test_api_mcp.py +++ b/tests/test_api_mcp.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from datetime import UTC, datetime, timedelta import pytest from fastapi.testclient import TestClient @@ -8,7 +9,7 @@ from app.db import create_schema, session_scope from app.ledger.service import close_bounty, create_bounty, ensure_genesis, pay_bounty from app.main import create_app -from app.models import Proof +from app.models import BountyAttempt, Proof def test_health_status_and_bounty_api(sqlite_url: str) -> None: @@ -1255,10 +1256,88 @@ def test_mcp_submit_work_proof_returns_structured_bounty_guidance(sqlite_url: st assert structured["issue_url"] == "https://github.com/ramimbo/mergework/issues/315" assert structured["title"] == "Structured MCP work-proof guidance" assert structured["acceptance"] == "Return machine-readable work-proof guidance." + assert structured["active_attempts"] == [] + assert structured["attempt_warnings"] == [] + assert structured["attempt_registration"] == { + "method": "POST", + "path": f"/api/v1/bounties/{bounty_id}/attempts", + "ttl_seconds_default": 86400, + "ttl_seconds_min": 60, + "ttl_seconds_max": 604800, + "advisory_only": True, + } + assert "register an advisory attempt" in structured["submission_format"] assert "/claim" in structured["submission_format"] assert "private keys" in structured["safety_rules"][0] +def test_mcp_submit_work_proof_structured_guidance_includes_attempt_state( + sqlite_url: str, +) -> None: + create_schema(sqlite_url) + now = datetime.now(UTC) + with session_scope(sqlite_url) as session: + ensure_genesis(session) + bounty = create_bounty( + session, + repo="ramimbo/mergework", + issue_number=321, + issue_url="https://github.com/ramimbo/mergework/issues/321", + title="Attempt reservations", + reward_mrwk="250", + max_awards=2, + acceptance="Register active attempts before opening overlapping PRs.", + ) + bounty_id = bounty.id + session.add_all( + [ + BountyAttempt( + bounty_id=bounty_id, + submitter_account="github:alice", + source_url="https://github.com/ramimbo/mergework/pull/500", + status="active", + expires_at=now + timedelta(hours=1), + created_at=now - timedelta(minutes=10), + updated_at=now - timedelta(minutes=10), + ), + BountyAttempt( + bounty_id=bounty_id, + submitter_account="github:bob", + source_url="https://github.com/ramimbo/mergework/pull/501", + status="active", + expires_at=now + timedelta(hours=1), + created_at=now - timedelta(minutes=5), + updated_at=now - timedelta(minutes=5), + ), + ] + ) + + client = TestClient(create_app(database_url=sqlite_url, webhook_secret="secret")) + result = client.post( + "/mcp", + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "submit_work_proof", + "arguments": {"bounty_id": bounty_id, "format": "json"}, + }, + }, + ).json()["result"]["structuredContent"] + + assert result["attempt_warnings"] == ["bounty has 2 active attempts"] + assert [attempt["submitter_account"] for attempt in result["active_attempts"]] == [ + "github:bob", + "github:alice", + ] + assert result["active_attempts"][0]["source_url"] == ( + "https://github.com/ramimbo/mergework/pull/501" + ) + assert result["attempt_registration"]["path"] == f"/api/v1/bounties/{bounty_id}/attempts" + assert result["attempt_registration"]["advisory_only"] is True + + def test_mcp_submit_work_proof_returns_structured_generic_guidance(sqlite_url: str) -> None: create_schema(sqlite_url) with session_scope(sqlite_url) as session: @@ -1287,9 +1366,14 @@ def test_mcp_submit_work_proof_returns_structured_generic_guidance(sqlite_url: s "repository": None, "issue_url": None, "acceptance": None, + "active_attempts": [], + "attempt_warnings": ["select a bounty before inspecting or registering attempts"], + "attempt_registration": None, "submission_format": ( - "Open a focused PR or issue, reference the MRWK bounty, include test " - "evidence, and wait for a maintainer to apply mrwk:accepted." + "Select a concrete MRWK bounty first. For open bounties with award " + "slots, register an advisory attempt before opening a focused PR or " + "issue, reference the bounty, include test evidence, and wait for a " + "maintainer to apply mrwk:accepted." ), "safety_rules": [ "Do not include private keys, seed material, secrets, deployment "