mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
Compare commits
7 commits
3224d7abf4
...
f79f49c755
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f79f49c755 | ||
|
|
2533402f6b | ||
|
|
df96fe49b5 | ||
|
|
485674d941 | ||
|
|
719741f279 | ||
|
|
f3db9f02e3 | ||
|
|
3af0bd5e16 |
23 changed files with 202 additions and 9629 deletions
59
.github/dependabot.yml
vendored
59
.github/dependabot.yml
vendored
|
|
@ -1,52 +1,45 @@
|
|||
version: 2
|
||||
updates:
|
||||
# Python — uv (open-swe-specific; siblings use pip)
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
assignees: ["amoussa1229"]
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
major:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "major"
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
# JavaScript/TypeScript — CDK (/infra), Playwright (/tests/e2e), dashboard (/ui), root tooling
|
||||
- package-ecosystem: "npm"
|
||||
directories:
|
||||
- "/"
|
||||
- "/infra"
|
||||
- "/tests/e2e"
|
||||
- "/ui"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
assignees: ["amoussa1229"]
|
||||
groups:
|
||||
minor-and-patch:
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
# Docker — root Dockerfile
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
assignees: ["amoussa1229"]
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
major:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "major"
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
assignees: ["amoussa1229"]
|
||||
groups:
|
||||
minor-and-patch:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
major:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "major"
|
||||
update-types: ["minor", "patch"]
|
||||
|
|
|
|||
10
.github/scripts/check-dev-green.sh
vendored
10
.github/scripts/check-dev-green.sh
vendored
|
|
@ -4,7 +4,7 @@
|
|||
# Reads check-runs on stdin — one
|
||||
# name<US>status<US>conclusion<US>details_url
|
||||
# per line, fields separated by ASCII Unit Separator (0x1F) — so it is unit-testable
|
||||
# WITHOUT GitHub. promote_dev_to_prod.yml pipes the live `gh api .../check-runs`
|
||||
# WITHOUT GitHub. promote-dev-to-prod.yml pipes the live `gh api .../check-runs`
|
||||
# output in. 0x1F (not TAB) is used deliberately: TAB is IFS-whitespace, so an empty
|
||||
# conclusion (every in_progress check has a null conclusion) would collapse and shift
|
||||
# the columns — which would make the promote run fail to exclude itself. 0x1F is
|
||||
|
|
@ -26,12 +26,12 @@
|
|||
set -euo pipefail
|
||||
|
||||
EXCLUDE_RUN_ID="${EXCLUDE_RUN_ID:-}"
|
||||
# Mandatory checks (one per line). Defaults to the Agent CI suite, which runs on
|
||||
# Mandatory checks (one per line). Defaults to the CI suite, which runs on
|
||||
# every push to dev (see ci.yml). Keep in sync with those job names; if a name
|
||||
# drifts the gate blocks (fails safe) until the list is updated.
|
||||
REQUIRED_CHECKS="${REQUIRED_CHECKS:-Agent lint
|
||||
Agent format check
|
||||
Agent unit tests
|
||||
REQUIRED_CHECKS="${REQUIRED_CHECKS:-Lint
|
||||
Format check
|
||||
Unit tests
|
||||
Playwright E2E}"
|
||||
|
||||
declare -A GREEN
|
||||
|
|
|
|||
4
.github/workflows/build-artifacts.yml
vendored
4
.github/workflows/build-artifacts.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
BUCKET: open-swe-dev-assets
|
||||
DEPLOY_DOC: open-swe-dev-deploy
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
|
@ -100,7 +100,7 @@ jobs:
|
|||
BUCKET: open-swe-prod-assets
|
||||
DEPLOY_DOC: open-swe-prod-deploy
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
|
|
|||
4
.github/workflows/cd-infra.yml
vendored
4
.github/workflows/cd-infra.yml
vendored
|
|
@ -66,7 +66,7 @@ jobs:
|
|||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
|
|
|||
2
.github/workflows/ci-infra.yml
vendored
2
.github/workflows/ci-infra.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: Infra CI
|
||||
|
||||
# Path-filtered CI for the /infra CDK app (TypeScript). The existing "Agent CI"
|
||||
# Path-filtered CI for the /infra CDK app (TypeScript). The existing "CI"
|
||||
# (ci.yml) covers the Python agent; this adds tsc + jest + cdk synth for /infra so
|
||||
# infra changes are gated on a PR the same way. Runs only when /infra changes.
|
||||
|
||||
|
|
|
|||
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
|
|
@ -1,11 +1,11 @@
|
|||
name: Agent CI
|
||||
name: CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
# dev as well as main so every dev HEAD carries the full Agent CI signal that
|
||||
# dev as well as main so every dev HEAD carries the full CI signal that
|
||||
# the dev->main promotion gate (check-dev-green.sh) reads. PR checks alone are
|
||||
# not enough: an admin-merge can land a red PR onto dev.
|
||||
branches: ["main", "dev"]
|
||||
|
|
@ -18,33 +18,33 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
lint:
|
||||
name: Agent lint
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
- name: Install dependencies
|
||||
run: uv sync --locked --extra dev
|
||||
- name: Run lint
|
||||
run: make lint
|
||||
|
||||
format:
|
||||
name: Agent format check
|
||||
name: Format check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
- name: Install dependencies
|
||||
run: uv sync --locked --extra dev
|
||||
- name: Run format check
|
||||
run: make format-check
|
||||
|
||||
unit-tests:
|
||||
name: Agent unit tests
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
- name: Install dependencies
|
||||
run: uv sync --locked --extra dev
|
||||
- name: Run unit tests
|
||||
|
|
@ -55,8 +55,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
|
|
|||
15
.github/workflows/dependency-review.yml
vendored
Normal file
15
.github/workflows/dependency-review.yml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
name: Dependency Review
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: high
|
||||
13
.github/workflows/labeler.yml
vendored
Normal file
13
.github/workflows/labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: Labeler
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@main
|
||||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
contents: write
|
||||
checks: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
|
|
@ -78,8 +78,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 360
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
- name: Install dependencies
|
||||
run: uv sync --locked
|
||||
- name: Run reviewer eval
|
||||
4
.github/workflows/rollback.yml
vendored
4
.github/workflows/rollback.yml
vendored
|
|
@ -50,7 +50,7 @@ jobs:
|
|||
DEPLOY_DOC: open-swe-dev-deploy
|
||||
TARGET_SHA: ${{ inputs.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_APP_DEV }}
|
||||
|
|
@ -75,7 +75,7 @@ jobs:
|
|||
DEPLOY_DOC: open-swe-prod-deploy
|
||||
TARGET_SHA: ${{ inputs.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_APP_PROD }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.14.5-slim-trixie
|
||||
FROM python:3.14.6-slim-trixie
|
||||
|
||||
ARG DOCKER_CLI_VERSION=5:29.1.5-1~debian.13~trixie
|
||||
ARG NODEJS_VERSION=22.22.0-1nodesource1
|
||||
|
|
|
|||
|
|
@ -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 bodies use `## Summary / Validation / Tests / Notes`, and commit messages follow the handbook format (≤50-char imperative subject, *why* over *what*). The **PR title rule is repo-aware**: when the target repo enforces a conventional-commit title (an `amannn/action-semantic-pull-request` workflow, a `commitlint` config, or a documented requirement in `AGENTS.md` / `CONTRIBUTING.md`), the agent emits a conforming `type(scope): …` title that reads the action's allowed types/scopes — this lets it pass gates like this repo's own `PR Title Lint` and upstream `langchain-ai/open-swe` without manual retitling; otherwise it falls back to the Sea Haven imperative style with no `type:` prefix. PRs that resolve a GitHub issue **auto-link it** in the body (`Closes #<n>` for full fixes, `Refs #<n>`/`Part of #<n>` for partial work, `Closes owner/repo#<n>` cross-repo); because the Sea Haven flow targets `dev` rather than the default branch, the issue closes when `dev` is promoted, not at dev-merge. **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.
|
||||
|
|
|
|||
|
|
@ -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,55 @@ 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):
|
||||
```
|
||||
<type>: <concise description> [closes <TICKET>]
|
||||
```
|
||||
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.
|
||||
**PR Title** (under 70 characters): the title rule is **repo-aware** — first detect whether the target repo enforces a conventional-commit PR title, then pick the matching style. The repo is already cloned, so this check is cheap.
|
||||
|
||||
**PR Body** (keep under 10 lines total. the more concise the better):
|
||||
```
|
||||
## Description
|
||||
<1-3 sentences on WHY and the approach.
|
||||
NO "Changes:" section — file changes are already in the commit history.>
|
||||
*Detect a conventional-commit title gate* — the repo enforces one if ANY of these hold:
|
||||
- a workflow under `.github/workflows/` references `amannn/action-semantic-pull-request` (or any `semantic-pull-request` action);
|
||||
- a `commitlint` config wired to PR titles (`commitlint.config.*`, `.commitlintrc*`, or a `commitlint` key in `package.json`);
|
||||
- `AGENTS.md` / `CONTRIBUTING.md` states a conventional-commit title requirement.
|
||||
|
||||
## Release Note
|
||||
<One-line changelog summary for self-hosted customers, or "none" for internal/CI/test/refactor changes.>
|
||||
*If a gate is enforced* → emit a conventional-commit title `type(scope): description` and conform to the action's configuration. This **overrides** the Sea Haven no-`type:`-prefix default. Open the workflow (e.g. `.github/workflows/pr_lint.yml`) and read the allowed `types`/`scopes` so you stay inside them; if `requireScope` is false, a scope is optional. Map the work to a type: new functionality → `feat`, defect fix → `fix`, infra/CI → `ci`/`build`/`chore`, docs → `docs`, tests → `test`, refactor → `refactor`, perf → `perf`. Examples: `feat: add retry logic for transient upstream failures` or `fix(deps): pin langgraph-cli`. Do NOT rely on an escape-hatch label (e.g. `ignore-lint-pr-title`) to dodge the check — conform to the title instead. (Note: this repo's own `PR Title Lint` and upstream `langchain-ai/open-swe` both enforce this — emit a conforming `type:` title for them.)
|
||||
|
||||
## Test Plan
|
||||
- [ ] <new/novel verification steps only — NOT "run existing tests" or "verify existing behavior">
|
||||
*If no gate is enforced* → use the Sea Haven imperative style: 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:
|
||||
```
|
||||
[<KEY>] Add retry logic for transient upstream failures
|
||||
```
|
||||
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** — use this structure. Omit a section only when it would be empty:
|
||||
```
|
||||
## Summary
|
||||
<What changed and why — 1-3 sentences. Explain the motivation, not just the diff.>
|
||||
|
||||
## Validation
|
||||
<How you verified it works — commands run, steps taken, screenshots if UI.>
|
||||
|
||||
## 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.>
|
||||
```
|
||||
|
||||
**Link the GitHub issue the PR resolves** — when the run originates from (or fully fixes) a GitHub issue, add a closing keyword to the PR body so merging auto-closes the issue. The issue number is usually in-context: issue-triggered runs receive a `## GitHub Issue: #<n>` line; for Slack/Linear-triggered runs that fix a GitHub issue, pick `#<n>` up from the task text.
|
||||
- When the PR **fully resolves** a GitHub issue in the **same repo**, add a dedicated trailing line in `## Summary` (or its own line at the end of the body): `Closes #<n>`. GitHub recognizes `Closes`/`Fixes`/`Resolves #<n>` anywhere in the body.
|
||||
- When the PR only **partially** addresses an issue (more work remains), use a **non-closing** reference so the issue stays open: `Refs #<n>` or `Part of #<n>`.
|
||||
- **Cross-repo**: if the issue lives in a different repo, use the fully-qualified form: `Closes owner/repo#<n>` (or `Refs owner/repo#<n>` for partial).
|
||||
- This is the GitHub-issue analog of the Linear `Refs: <KEY>` commit trailer — placed in the PR body where GitHub's auto-close looks.
|
||||
- **Default-branch caveat (don't mistake this for a bug):** GitHub only auto-closes the linked issue when the PR merges into the repo's **default branch**. In the Sea Haven flow the agent targets `dev`, not the default branch, so `Closes #<n>` will **not** close the issue at dev-merge time — it closes when `dev` is promoted to the default branch. The link still renders, and the issue closes on promotion; this is the correct, expected outcome. On repos where the agent targets the default branch directly, it closes on merge as usual.
|
||||
|
||||
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.
|
||||
|
||||
This per-commit convention is independent of the repo-aware **PR title** rule above. On a repo that requires conventional PR titles **and** squash-merges, the squash commit subject becomes the PR title (e.g. `feat: …`) and so diverges from this imperative-no-prefix commit style — that's an acceptable tradeoff (the target repo's title lint wins), not a contradiction. Your own per-commit subjects still follow the Sea Haven format here.
|
||||
|
||||
**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 +476,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 +495,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 = """---
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ uv run python -m evals.reviewer.run_eval --limit 3
|
|||
|
||||
### From the GitHub Action (recommended for full runs)
|
||||
|
||||
Trigger the **Reviewer eval** workflow (`.github/workflows/reviewer_eval.yml`)
|
||||
from the Actions UI or `gh workflow run reviewer_eval.yml --ref prod -f limit=3`.
|
||||
Trigger the **Reviewer eval** workflow (`.github/workflows/reviewer-eval.yml`)
|
||||
from the Actions UI or `gh workflow run reviewer-eval.yml --ref prod -f limit=3`.
|
||||
Run it on the **prod** branch so the harness/judge match the deployed reviewer it
|
||||
scores. Running it on a durable runner (instead of inside the serving deployment)
|
||||
means a deploy or container recycle can't kill a long run.
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ npm test # jest — naming Aspect
|
|||
## CI/CD (T18 — `.github/workflows/ci-infra.yml` + `cd-infra.yml`)
|
||||
|
||||
Path-filtered, OIDC-only (no static keys). The Python agent keeps its own
|
||||
`ci.yml` ("Agent CI"); these two add the `/infra` half.
|
||||
`ci.yml` ("CI"); these two add the `/infra` half.
|
||||
|
||||
| Workflow | Trigger | Does |
|
||||
|---|---|---|
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ dependencies = [
|
|||
"langgraph>=1.1.10",
|
||||
"markdownify>=1.2.2",
|
||||
"langchain-anthropic>=1.4.6",
|
||||
"langgraph-cli[inmem]>=0.4.27",
|
||||
"langgraph-cli[inmem]>=0.4.30",
|
||||
"langsmith==0.8.18",
|
||||
"langchain-openai>=1.2.2",
|
||||
"langchain-fireworks>=1.4.2",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,42 @@ 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_links_resolved_github_issue() -> None:
|
||||
prompt = construct_system_prompt(working_dir="/workspace")
|
||||
|
||||
# Part A: PRs that resolve a GitHub issue auto-link it for auto-close.
|
||||
assert "Closes #<n>" in prompt
|
||||
assert "Refs #<n>" in prompt or "Part of #<n>" in prompt
|
||||
assert "Closes owner/repo#<n>" in prompt
|
||||
# The default-branch auto-close caveat must be stated so it isn't read as a bug.
|
||||
assert "default branch" in prompt
|
||||
assert "promoted" in prompt or "promotion" in prompt
|
||||
|
||||
|
||||
def test_construct_system_prompt_pr_title_rule_is_repo_aware() -> None:
|
||||
prompt = construct_system_prompt(working_dir="/workspace")
|
||||
|
||||
# Part B: detect a conventional-commit title gate and conform to it...
|
||||
assert "amannn/action-semantic-pull-request" in prompt
|
||||
assert "repo-aware" in prompt
|
||||
assert "type(scope): description" in prompt or "type(scope): …" in prompt
|
||||
# ...without dropping the no-prefix default for repos that don't enforce one.
|
||||
assert "Do NOT use a conventional-commit `type:` prefix" in prompt
|
||||
|
||||
|
||||
def test_construct_system_prompt_shell_escapes_user_name() -> None:
|
||||
|
|
@ -217,38 +239,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(
|
||||
{
|
||||
|
|
|
|||
9386
ui/yarn.lock
9386
ui/yarn.lock
File diff suppressed because it is too large
Load diff
8
uv.lock
generated
8
uv.lock
generated
|
|
@ -1627,7 +1627,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "langgraph-cli"
|
||||
version = "0.4.27"
|
||||
version = "0.4.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
|
|
@ -1636,9 +1636,9 @@ dependencies = [
|
|||
{ name = "pathspec" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/21/046b2345d83427a59ebdac797fbe343bb8d1e575560667846f7346733774/langgraph_cli-0.4.27.tar.gz", hash = "sha256:67a64b67dddc8c670d77cc4c9663a7f8e924eaeb8857a926e87f239b699ef8f6", size = 1045499, upload-time = "2026-05-28T14:25:35.13Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/27/4b6a0f00c804f0b0831f741c0607b46a4cbddff14d1eab6bbd4ce5820837/langgraph_cli-0.4.30.tar.gz", hash = "sha256:4948fdc77ff45fc5ef3d8330d17bbecfcb26cd9c4d3a4f00da84a41a0226cd72", size = 1046771, upload-time = "2026-06-16T19:46:27.949Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/89/4bda72ff33172ad15ddb2c57c5b998bf9309e7c4890d3d84a8c8ed6333d4/langgraph_cli-0.4.27-py3-none-any.whl", hash = "sha256:d53bddfc4e7b6e47871bedb34ccc476fd5e4e97f8678dd453ecac597c5f11ff1", size = 78002, upload-time = "2026-05-28T14:25:34.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/b6/94cbd2ba0820caae203a915272394c576a21ab4a56dfbc93724dc8cd8e2b/langgraph_cli-0.4.30-py3-none-any.whl", hash = "sha256:9c577750c57da1a0e3407e8b83e5a0d7eaa80685fe99d95aa7f9bf0e1e73ca92", size = 82061, upload-time = "2026-06-16T19:46:26.873Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
@ -2040,7 +2040,7 @@ requires-dist = [
|
|||
{ name = "langchain-openai", specifier = ">=1.2.2" },
|
||||
{ name = "langchain-runloop", specifier = ">=0.0.4" },
|
||||
{ name = "langgraph", specifier = ">=1.1.10" },
|
||||
{ name = "langgraph-cli", extras = ["inmem"], specifier = ">=0.4.27" },
|
||||
{ name = "langgraph-cli", extras = ["inmem"], specifier = ">=0.4.30" },
|
||||
{ name = "langgraph-sdk", specifier = ">=0.4.2" },
|
||||
{ name = "langsmith", specifier = "==0.8.18" },
|
||||
{ name = "markdownify", specifier = ">=1.2.2" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue