Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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 "
Expand All @@ -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 "
Expand Down
13 changes: 13 additions & 0 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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":"<proof_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":<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`
Expand Down
90 changes: 87 additions & 3 deletions tests/test_api_mcp.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import annotations

import json
from datetime import UTC, datetime, timedelta

import pytest
from fastapi.testclient import TestClient

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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 "
Expand Down
Loading