diff --git a/agent/prompt.py b/agent/prompt.py index ec7a45bb..7a95f3a4 100644 --- a/agent/prompt.py +++ b/agent/prompt.py @@ -6,8 +6,8 @@ from pathlib import Path from .utils.authorship import ( OPEN_SWE_BOT_EMAIL, OPEN_SWE_BOT_NAME, - PR_ATTRIBUTION_FOOTER, CollaboratorIdentity, + build_pr_attribution_footer, ) from .utils.github_comments import UNTRUSTED_GITHUB_COMMENT_OPEN_TAG @@ -375,7 +375,7 @@ This run was triggered by **{display_name}**. You author the work **as them** {bot_coauthor_trailer} ``` -- **PR body**: append this line to the bottom of the PR description (separated from the body by a blank line) when you open or update the draft PR. Do not duplicate it if it is already present. If the PR body already contains a legacy footer like `_Opened collaboratively by {display_name} and open-swe._`, replace that legacy footer with this line instead of appending a second footer: +- **PR body**: append this line to the bottom of the PR description (separated from the body by a blank line) when you open or update the draft PR. Do not duplicate it if it is already present. If the PR body already contains a `Made by [Open SWE]` footer pointing at a different link, or a legacy footer like `_Opened collaboratively by {display_name} and open-swe._`, replace that existing footer with this line instead of appending a second footer: ``` {pr_attribution_footer} @@ -384,12 +384,15 @@ This run was triggered by **{display_name}**. You author the work **as them** If you forget the trailer on a local commit that has not been pushed, fix it with `git commit --amend` before pushing — do not push without it. If the commit has already been pushed, leave it as-is and add the trailer to your next commit; never rewrite remote history to fix it.""" -def _render_collaboration_section(identity: CollaboratorIdentity | None) -> str: +def _render_collaboration_section( + identity: CollaboratorIdentity | None, + thread_url: str | None = None, +) -> str: if identity is None: return "" return COLLABORATION_TEMPLATE.format( display_name=identity.display_name, - pr_attribution_footer=PR_ATTRIBUTION_FOOTER, + pr_attribution_footer=build_pr_attribution_footer(thread_url), bot_coauthor_trailer=f"Co-authored-by: {OPEN_SWE_BOT_NAME} <{OPEN_SWE_BOT_EMAIL}>", ) @@ -446,6 +449,7 @@ def construct_system_prompt( create_prs: bool = False, default_repo: dict[str, str] | None = None, repo_custom_instructions: str | None = None, + thread_url: str | None = None, ) -> str: default_prompt_section = _load_default_prompt() if default_repo and default_repo.get("owner") and default_repo.get("name"): @@ -468,7 +472,7 @@ def construct_system_prompt( linear_issue_number=linear_issue_number or "", default_prompt_section=default_prompt_section, pr_policy_override_section=ALWAYS_CREATE_PR_SECTION if create_prs else "", - collaboration_section=_render_collaboration_section(triggering_user_identity), + collaboration_section=_render_collaboration_section(triggering_user_identity, thread_url), repo_instructions_section=_render_repo_instructions_section(repo_custom_instructions), commit_identity_name=commit_identity_name, commit_identity_email=commit_identity_email, diff --git a/agent/server.py b/agent/server.py index d8cb4ce7..eb3beb66 100644 --- a/agent/server.py +++ b/agent/server.py @@ -81,6 +81,7 @@ from .utils.authorship import ( OPEN_SWE_BOT_NAME, resolve_triggering_user_identity, ) +from .utils.dashboard_links import dashboard_thread_url from .utils.github_app import ( get_github_app_installation_token_with_expiry, ) @@ -691,6 +692,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: create_prs=always_create_prs, default_repo=prompt_default_repo, repo_custom_instructions=repo_custom_instructions, + thread_url=dashboard_thread_url(thread_id), ), tools=[ http_request, diff --git a/agent/utils/authorship.py b/agent/utils/authorship.py index 4e9a8ba8..a9e5a2d7 100644 --- a/agent/utils/authorship.py +++ b/agent/utils/authorship.py @@ -16,7 +16,15 @@ OPEN_SWE_BOT_NAME = "open-swe[bot]" # accepts, which broke preview deploys on commits carrying this co-author. OPEN_SWE_BOT_EMAIL = "open-swe@users.noreply.github.com" -PR_ATTRIBUTION_FOOTER = "Made by [Open SWE](https://openswe.vercel.app)" +PR_ATTRIBUTION_TEXT = "Made by [Open SWE]" +PR_ATTRIBUTION_DEFAULT_URL = "https://openswe.vercel.app" +PR_ATTRIBUTION_FOOTER = f"{PR_ATTRIBUTION_TEXT}({PR_ATTRIBUTION_DEFAULT_URL})" + + +def build_pr_attribution_footer(thread_url: str | None = None) -> str: + """Build the Open SWE PR footer, linking the run's thread when available.""" + url = thread_url.strip() if isinstance(thread_url, str) and thread_url.strip() else "" + return f"{PR_ATTRIBUTION_TEXT}({url or PR_ATTRIBUTION_DEFAULT_URL})" @dataclass(frozen=True) @@ -159,17 +167,21 @@ def add_bot_coauthor_trailer(commit_message: str) -> str: def add_pr_collaboration_note( pr_body: str, identity: CollaboratorIdentity | None = None, + thread_url: str | None = None, ) -> str: """Append the Open SWE attribution footer to a PR body. The PR is opened as the triggering user, so the body only credits Open SWE - as the collaborator. Any legacy double-attribution footer is replaced. + as the collaborator. The footer links the run's thread when available. Any + legacy double-attribution footer is replaced. """ normalized_body = pr_body.rstrip() - note = PR_ATTRIBUTION_FOOTER + note = build_pr_attribution_footer(thread_url) if note in normalized_body: return normalized_body + if PR_ATTRIBUTION_TEXT in normalized_body: + return normalized_body legacy_footers: list[str] = [] if identity is not None: diff --git a/tests/test_github_comment_prompts.py b/tests/test_github_comment_prompts.py index de2f759a..8f212323 100644 --- a/tests/test_github_comment_prompts.py +++ b/tests/test_github_comment_prompts.py @@ -126,12 +126,27 @@ def test_construct_system_prompt_includes_github_login_in_pr_footer() -> None: assert "git config user.email 1234+octocat@users.noreply.github.com" in prompt assert _BOT_TRAILER in prompt assert "Made by [Open SWE](https://openswe.vercel.app)" in prompt - assert ( - "replace that legacy footer with this line instead of appending a second footer" in prompt - ) + assert "replace that existing footer with this line" in prompt assert "`_Opened collaboratively by Mona Lisa and open-swe._`" in prompt +def test_construct_system_prompt_footer_links_thread_when_provided() -> None: + identity = CollaboratorIdentity( + display_name="octocat", + commit_name="octocat", + commit_email="1234+octocat@users.noreply.github.com", + ) + + prompt = construct_system_prompt( + working_dir="/workspace", + triggering_user_identity=identity, + thread_url="https://openswe.vercel.app/agents/abc-123", + ) + + assert "Made by [Open SWE](https://openswe.vercel.app/agents/abc-123)" in prompt + assert "Made by [Open SWE](https://openswe.vercel.app)" not in prompt + + def test_construct_system_prompt_shell_escapes_user_name() -> None: import shlex @@ -168,6 +183,23 @@ def test_add_pr_collaboration_note_replaces_legacy_footer() -> None: ) +def test_add_pr_collaboration_note_links_thread() -> None: + body = "## Description\nDone." + + assert add_pr_collaboration_note( + body, thread_url="https://openswe.vercel.app/agents/abc-123" + ) == ("## Description\nDone.\n\nMade by [Open SWE](https://openswe.vercel.app/agents/abc-123)") + + +def test_add_pr_collaboration_note_skips_when_footer_present_with_other_link() -> None: + body = "## Description\nDone.\n\nMade by [Open SWE](https://openswe.vercel.app)" + + assert ( + add_pr_collaboration_note(body, thread_url="https://openswe.vercel.app/agents/abc-123") + == body + ) + + def test_resolve_triggering_user_identity_combines_slack_name_with_github_login() -> None: identity = resolve_triggering_user_identity( {