diff --git a/README.md b/README.md index 89ec3a9e..2f123e58 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,10 @@ All three companies in the article converge on **Slack as the primary invocation Each invocation creates a deterministic thread ID, so follow-up messages on the same issue or thread route to the same running agent. +**Trigger tags (Sea Haven fork):** a mention is a case-insensitive substring match on the comment body — `@openswe`, `@open-swe`, `@openswe-dev`, or `@seahaven-openswe` (the deployed App slug). GitHub won't linkify `@seahaven-openswe` (App `[bot]` accounts aren't user-mentionable), but the text still fires a run. + +**Engineering conventions & attribution (Sea Haven fork):** the main agent's system prompt is tuned to the Sea Haven engineering handbook — branch names are `feature|bug|hotfix/` (optional resolvable `-` prefix), PR titles are imperative with no conventional-commit `type:` prefix, PR bodies use `## Summary / Validation / Tests / Notes`, and commit messages follow the handbook format (≤50-char imperative subject, *why* over *what*). **No agent/AI attribution is added to any artifact** — no `Co-authored-by` bot trailer, no `Made by [Open SWE]` footer, no "generated by an agent" notes. Commits are currently authored as the **triggering user** (the upstream behavior, which keeps Vercel preview deploys resolvable); flipping authorship to the bot account is tracked separately in issue #11 pending the Vercel-resolvability decision. + ### 7. Validation — Prompt-Driven The agent is instructed to run linters, formatters, and tests before committing, and is responsible end-to-end for committing, pushing, opening/updating the draft PR, and replying in the source channel. diff --git a/agent/prompt.py b/agent/prompt.py index 45ee49d0..ffe9c03d 100644 --- a/agent/prompt.py +++ b/agent/prompt.py @@ -7,7 +7,6 @@ from .utils.authorship import ( OPEN_SWE_BOT_EMAIL, OPEN_SWE_BOT_NAME, CollaboratorIdentity, - build_pr_attribution_footer, ) from .utils.github_comments import UNTRUSTED_GITHUB_COMMENT_OPEN_TAG @@ -159,7 +158,12 @@ Before starting any task that requires code changes, set up the repository in yo This sets the author of every commit you make. This is required for CI: third-party integrations (e.g. Vercel preview deploys) reject commits whose author email cannot be resolved to a GitHub account, and this email resolves. Do NOT set any other identity, do NOT pass `--author` to `git commit`, and do NOT export `GIT_AUTHOR_*` / `GIT_COMMITTER_*` env vars. -4. **Choose your branch** — Use a thread-stable branch name such as `open-swe/`. If a branch already exists for this thread/task, fetch and check it out instead of creating a new one. +4. **Choose your branch** — Use a Sea Haven branch name: `/`, all kebab-case. Pick the prefix by the kind of work: + - `feature/` — new functionality or an enhancement + - `bug/` — a defect caught before it reaches production + - `hotfix/` — a fix for a production-impacting issue + + Keep `` short and kebab-case (e.g. `feature/add-receipt-parser`). When a ticket key is resolvable from the run context, put it first: `feature/-add-receipt-parser`; if no key is resolvable, omit it. Never commit directly to `main`. Keep the branch thread-stable: if a branch already exists for this thread/task, fetch and check it out instead of creating a new one. 5. **Checkout your branch** — Always fetch and checkout your branch before making any changes. When reusing an existing remote branch, start from `origin/` rather than recreating the branch from the base branch; this preserves prior commits for review. @@ -390,31 +394,37 @@ When you have completed your implementation, follow these steps in order: - **Open a new PR** with the `open_pull_request` tool (pass `owner`, `repo`, `head` = your branch, `base`, `title`, `body`). This attributes the PR to the triggering user. Push the branch BEFORE calling it. - **Update an existing PR** (edit the body, mark ready for review, etc.) with `GH_TOKEN=dummy gh pr edit`. If a PR already exists for the branch (including one the user pasted in), do NOT open a duplicate — `open_pull_request` returns the existing PR's URL, so switch to `gh pr edit`. For follow-up changes, add a new commit on top of the existing branch history. - **PR Title** (under 70 characters): + **PR Title** (under 70 characters): imperative mood, capitalized, describing the change — not the ticket. Do NOT use a conventional-commit `type:` prefix (no `feat:`/`fix:`/`chore:`). When a ticket key is resolvable from the run context, prefix it in square brackets; otherwise omit it entirely: ``` - : [closes ] + [] Add retry logic for transient upstream failures ``` - Where type is one of: `fix` (bug fix), `feat` (new feature), `chore` (maintenance), `ci` (CI/CD). - Always append the resolvable ticket number in square brackets at the end of the title (e.g. `fix: handle null session [closes AB-000]`). Resolve the ticket from the Linear-triggered run when present (`{linear_project_id}-{linear_issue_number}`), or from a Linear ticket referenced in the Slack thread / task context. If no ticket number is resolvable, omit the bracketed suffix entirely. + With no resolvable key, use just the imperative description: `Add retry logic for transient upstream failures`. Resolve the key from the Linear-triggered run when present (`{linear_project_id}-{linear_issue_number}`), or from a Linear ticket referenced in the Slack thread / task context. - **PR Body** (keep under 10 lines total. the more concise the better): + **PR Body** — use this structure. Omit a section only when it would be empty: ``` - ## Description - <1-3 sentences on WHY and the approach. - NO "Changes:" section — file changes are already in the commit history.> + ## Summary + - ## Release Note - + ## Validation + - ## Test Plan - - [ ] + ## Tests + + + ## Notes + ``` You don't need to add links back to the originating Slack thread or Linear ticket — for private repos, `open_pull_request` appends a `## References` section automatically. When the target repo is public, don't reference private repos or private PR/issue numbers in the description. - **Commit message**: Concise, focusing on the "why" rather than the "what". If not provided, the PR title is used. + **Commit message** — follow the Sea Haven format: + - Imperative mood, capitalized first letter (e.g. "Add retry logic", not "Added retry logic" or "adds retry logic"). + - Subject line ≤50 characters. If you need more, add a blank line and a body wrapped at 72 characters. + - Explain *why*, not *what* — the diff already shows what changed. + - No generic subjects ("Fix stuff", "Update code", "WIP", "Address review comments") and no self-referential phrasing ("This commit…", "This PR…", "I refactored…"). + - When a ticket key is resolvable, add a `Refs: ` trailer (combine with `#` when both apply); otherwise omit the trailer. **IMPORTANT: For code-change tasks, never ask the user for permission or confirmation before pushing commits or opening/updating a draft PR. Do not say "if you want, I can proceed" or "shall I open the PR?". When implementation is done and checks pass, push autonomously, and open/update a draft PR autonomously when requested, necessary, or required by the Always Create PRs dashboard setting.** @@ -448,23 +458,17 @@ For code-change tasks, push the branch and notify the appropriate source once im COLLABORATION_TEMPLATE = """--- -### Collaborative Attribution +### Authorship & Attribution -This run was triggered by **{display_name}**. You author the work **as them** — their git identity is already configured in the Repository Setup step, so every commit and the PR are attributed to them. Credit open-swe as the collaborator: +This run was triggered by **{display_name}**. You author the work as them — their git identity is already configured in the Repository Setup step, so every commit and the PR are attributed to them. -- **Commits**: append this trailer (verbatim, on its own line, separated from the message body by a blank line) to every commit message you author. Add it to both the first commit and any follow-up commits in this run: +**Add NO agent or AI attribution to any artifact.** Sea Haven artifacts carry no agent attribution — write commits, PR descriptions, and comments exactly as a human engineer would. Specifically, never add: - ``` - {bot_coauthor_trailer} - ``` +- A `Co-authored-by:` trailer for any bot or agent (no `open-swe[bot]`, no `Claude`, etc.). +- A PR-body footer or tagline such as `Made by [Open SWE]`, `Generated with …`, a 🤖 emoji line, or `_Opened collaboratively by … and open-swe._`. +- Any "created/opened by an agent" note in commits, PR bodies, or issue comments. -- **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} - ``` - -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.""" +If a template or a prior artifact already contains such attribution, strip it rather than carrying it forward.""" def _render_collaboration_section( @@ -473,11 +477,7 @@ def _render_collaboration_section( ) -> str: if identity is None: return "" - return COLLABORATION_TEMPLATE.format( - display_name=identity.display_name, - pr_attribution_footer=build_pr_attribution_footer(thread_url), - bot_coauthor_trailer=f"Co-authored-by: {OPEN_SWE_BOT_NAME} <{OPEN_SWE_BOT_EMAIL}>", - ) + return COLLABORATION_TEMPLATE.format(display_name=identity.display_name) ALWAYS_CREATE_PR_SECTION = """--- diff --git a/agent/utils/authorship.py b/agent/utils/authorship.py index a9e5a2d7..a632f7b2 100644 --- a/agent/utils/authorship.py +++ b/agent/utils/authorship.py @@ -1,4 +1,4 @@ -"""Helpers for collaborative commit and PR attribution.""" +"""Helpers for resolving the triggering user's git identity.""" from __future__ import annotations @@ -16,16 +16,6 @@ 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_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) class CollaboratorIdentity: @@ -149,50 +139,3 @@ def resolve_triggering_user_identity( """ return _identity_from_github_token(github_token) or _identity_from_config(config) - - -def add_bot_coauthor_trailer(commit_message: str) -> str: - """Append the open-swe[bot] Co-authored-by trailer. - - Commits are authored by the triggering user (via the repo-local git - identity); open-swe[bot] is credited as the collaborator. - """ - normalized_message = commit_message.rstrip() - trailer = f"Co-authored-by: {OPEN_SWE_BOT_NAME} <{OPEN_SWE_BOT_EMAIL}>" - if trailer in normalized_message: - return normalized_message - return f"{normalized_message}\n\n{trailer}" - - -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. The footer links the run's thread when available. Any - legacy double-attribution footer is replaced. - """ - - normalized_body = pr_body.rstrip() - 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: - legacy_footers.append( - f"_Opened collaboratively by {identity.pr_attribution_name} and open-swe._" - ) - legacy_footers.append(f"_Opened collaboratively by {identity.display_name} and open-swe._") - for legacy in legacy_footers: - if legacy in normalized_body: - return normalized_body.replace(legacy, note) - - if not normalized_body: - return note - return f"{normalized_body}\n\n{note}" diff --git a/agent/utils/github_comments.py b/agent/utils/github_comments.py index af85b996..848f67d4 100644 --- a/agent/utils/github_comments.py +++ b/agent/utils/github_comments.py @@ -31,7 +31,7 @@ __all__ = [ "verify_github_signature", ] -OPEN_SWE_TAGS = ("@openswe", "@open-swe", "@openswe-dev") +OPEN_SWE_TAGS = ("@openswe", "@open-swe", "@openswe-dev", "@seahaven-openswe") UNTRUSTED_GITHUB_COMMENT_OPEN_TAG = "" UNTRUSTED_GITHUB_COMMENT_CLOSE_TAG = "" _SANITIZED_UNTRUSTED_GITHUB_COMMENT_OPEN_TAG = "[blocked-untrusted-comment-tag-open]" diff --git a/tests/test_authorship.py b/tests/test_authorship.py index bb021289..4df99801 100644 --- a/tests/test_authorship.py +++ b/tests/test_authorship.py @@ -1,23 +1,6 @@ from __future__ import annotations -from agent.utils.authorship import ( - OPEN_SWE_BOT_EMAIL, - OPEN_SWE_BOT_NAME, - add_bot_coauthor_trailer, - resolve_triggering_user_identity, -) - -_BOT_TRAILER = f"Co-authored-by: {OPEN_SWE_BOT_NAME} <{OPEN_SWE_BOT_EMAIL}>" - - -def test_add_bot_coauthor_trailer_appends_bot() -> None: - result = add_bot_coauthor_trailer("fix: thing") - assert result == f"fix: thing\n\n{_BOT_TRAILER}" - - -def test_add_bot_coauthor_trailer_is_idempotent() -> None: - once = add_bot_coauthor_trailer("fix: thing") - assert add_bot_coauthor_trailer(once) == once +from agent.utils.authorship import resolve_triggering_user_identity def test_resolve_identity_from_config_uses_user_noreply_email() -> None: diff --git a/tests/test_github_comment_prompts.py b/tests/test_github_comment_prompts.py index 88914a10..b5ebeeeb 100644 --- a/tests/test_github_comment_prompts.py +++ b/tests/test_github_comment_prompts.py @@ -8,7 +8,6 @@ from agent.utils.authorship import ( OPEN_SWE_BOT_EMAIL, OPEN_SWE_BOT_NAME, CollaboratorIdentity, - add_pr_collaboration_note, resolve_triggering_user_identity, ) @@ -100,7 +99,7 @@ def test_construct_system_prompt_includes_corridor_prompt_when_enabled() -> None def test_construct_system_prompt_omits_collaboration_section_without_identity() -> None: prompt = construct_system_prompt(working_dir="/workspace") - assert "Collaborative Attribution" not in prompt + assert "Authorship & Attribution" not in prompt assert "Co-authored-by:" not in prompt @@ -136,7 +135,7 @@ def test_construct_system_prompt_forbids_force_push() -> None: assert "git pull --rebase origin " in prompt -def test_construct_system_prompt_includes_coauthor_trailer_when_identity_present() -> None: +def test_construct_system_prompt_emits_no_attribution_when_identity_present() -> None: identity = CollaboratorIdentity( display_name="octocat", commit_name="octocat", @@ -148,16 +147,24 @@ def test_construct_system_prompt_includes_coauthor_trailer_when_identity_present triggering_user_identity=identity, ) - assert "Collaborative Attribution" in prompt - # The user authors the commits; open-swe[bot] is the co-author/collaborator. + # The Sea Haven Authorship section renders, the commits are still authored as + # the triggering user (identity flip to the bot is deferred — see issue #11), + # and NO agent/AI attribution leaks into the prompt. + assert "Authorship & Attribution" in prompt + assert "Add NO agent or AI attribution" in prompt # Values are shell-escaped via shlex.quote; safe tokens need no quoting. assert "git config user.name octocat" in prompt 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 + # The concrete attribution artifacts the old template INSTRUCTED are gone (the + # prohibition still names them as examples, so assert the instructional forms: + # the full co-author trailer, the URL-bearing footer, and the old mandate text). + assert _BOT_TRAILER not in prompt + assert "Made by [Open SWE](https://openswe.vercel.app)" not in prompt + assert "Credit open-swe as the collaborator" not in prompt + assert "append this trailer" not in prompt -def test_construct_system_prompt_includes_github_login_in_pr_footer() -> None: +def test_construct_system_prompt_no_attribution_with_github_login() -> None: identity = CollaboratorIdentity( display_name="Mona Lisa", commit_name="Mona Lisa", @@ -173,27 +180,19 @@ def test_construct_system_prompt_includes_github_login_in_pr_footer() -> None: # A name with a space is shlex-quoted; the safe email is left bare. assert "git config user.name 'Mona Lisa'" in prompt 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 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 _BOT_TRAILER not in prompt assert "Made by [Open SWE](https://openswe.vercel.app)" not in prompt + assert "replace that existing footer with this line" not in prompt + + +def test_construct_system_prompt_uses_sea_haven_conventions() -> None: + prompt = construct_system_prompt(working_dir="/workspace") + + # Branch naming, PR structure, and commit format follow the handbook. + assert "feature/" in prompt and "hotfix/" in prompt + assert "Do NOT use a conventional-commit `type:` prefix" in prompt + assert "## Summary" in prompt and "## Validation" in prompt + assert "## Release Note" not in prompt def test_construct_system_prompt_shell_escapes_user_name() -> None: @@ -217,38 +216,6 @@ def test_construct_system_prompt_shell_escapes_user_name() -> None: assert f"git config user.name {hostile}" not in prompt -def test_add_pr_collaboration_note_replaces_legacy_footer() -> None: - identity = CollaboratorIdentity( - display_name="Mona Lisa", - commit_name="Mona Lisa", - commit_email="1234+octocat@users.noreply.github.com", - github_login="octocat", - ) - - body = "## Description\nDone.\n\n_Opened collaboratively by Mona Lisa and open-swe._" - - assert add_pr_collaboration_note(body, identity) == ( - "## Description\nDone.\n\nMade by [Open SWE](https://openswe.vercel.app)" - ) - - -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( {