Adopt Sea Haven agent conventions, no attribution (#30)

Codify the box-only #4 customizations into Git so the AWS deployment
(which deploys from this repo) actually applies them — previously only
the retired sh-openswe box had them.

- prompt.py: branch names feature|bug|hotfix/<kebab> (optional <KEY->);
  imperative PR titles with no conventional-commit type: prefix; PR body
  Summary/Validation/Tests/Notes; handbook commit format. Rewrite the
  collaboration template from an attribution MANDATE to a PROHIBITION —
  no Co-authored-by bot trailer, no "Made by [Open SWE]" footer, no
  agent/AI notes on any artifact.
- github_comments.py: add @seahaven-openswe (the deployed App slug) to
  the mention triggers.
- authorship.py: remove the now-unused attribution helpers
  (build_pr_attribution_footer, add_bot_coauthor_trailer,
  add_pr_collaboration_note, PR_ATTRIBUTION_*). Keep OPEN_SWE_BOT_* —
  server.py still uses them for the sandbox git identity.
- Flip the attribution unit tests to assert the no-attribution behavior;
  drop tests for the removed helpers.

Commits stay authored as the triggering user for now — flipping
authorship to the bot account depends on the Vercel preview-deploy
constraint and is deferred to #11.

Refs: #4 #11

Claude-Session: https://claude.ai/code/session_01DMhLf4G5V8MStJQyAW95hi
This commit is contained in:
Adam Moussa 2026-06-27 22:08:45 -04:00 • committed by GitHub
parent 3af0bd5e16
commit f3db9f02e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 67 additions and 170 deletions

View file

@ -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/<kebab-desc>` (optional resolvable `<KEY>-` 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.

View file

@ -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/<short-task-slug>`. 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: `<prefix>/<description>`, 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 `<description>` short and kebab-case (e.g. `feature/add-receipt-parser`). When a ticket key is resolvable from the run context, put it first: `feature/<KEY>-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/<branch>` 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:
```
<type>: <concise description> [closes <TICKET>]
[<KEY>] 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
<What changed and why — 1-3 sentences. Explain the motivation, not just the diff.>
## Release Note
<One-line changelog summary for self-hosted customers, or "none" for internal/CI/test/refactor changes.>
## Validation
<How you verified it works — commands run, steps taken, screenshots if UI.>
## Test Plan
- [ ] <new/novel verification steps only — NOT "run existing tests" or "verify existing behavior">
## Tests
<What tests were added, updated, or run. If no automated tests, explain manual testing.>
## Notes
<Anything reviewers should know — migration steps, deploy order, follow-ups, breaking changes. Omit this section if empty.>
```
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: <KEY>` trailer (combine with `#<issue>` 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 = """---

View file

@ -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}"

View file

@ -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 = "<dangerous-external-untrusted-users-comment>"
UNTRUSTED_GITHUB_COMMENT_CLOSE_TAG = "</dangerous-external-untrusted-users-comment>"
_SANITIZED_UNTRUSTED_GITHUB_COMMENT_OPEN_TAG = "[blocked-untrusted-comment-tag-open]"

View file

@ -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:

View file

@ -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 <branch>" 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(
{