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
21 changes: 20 additions & 1 deletion .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ permissions:
issues: write

concurrency:
# One group per PR across all three trigger paths; new run cancels stale review run
group: opencode-review-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
cancel-in-progress: true

Expand All @@ -49,9 +50,11 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
# The runner workspace has no git repo on issue_comment (main-branch)
# runs, so gh cannot infer the repo — pass -R explicitly.
if [ "${{ github.event_name }}" = "issue_comment" ]; then
PR_NUMBER="${{ github.event.issue.number }}"
REF=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid')
REF=$(gh pr view -R "${{ github.repository }}" "$PR_NUMBER" --json headRefOid --jq '.headRefOid')
echo "ref=$REF" >> "$GITHUB_OUTPUT"
echo "Checked out PR #$PR_NUMBER at $REF"
else
Expand All @@ -77,6 +80,22 @@ jobs:
conventions, then analyze the diff and delegate to your specialist
subagents as appropriate. Post your synthesized review as a comment.

The PR thread is included as context (in <pull_request_comments> and
<pull_request_reviews>). It contains ALL previous comments and reviews
on this PR, including reviews from earlier runs of this bot.

DEDUPLICATION REQUIREMENT:
- Before reporting any finding, check whether that exact issue was
already reported in <pull_request_comments> or <pull_request_reviews>.
- Do NOT repeat findings that were already reported. Never restate an
existing comment.
- For each previously reported issue, only mention it again if its
status changed (e.g. was FIXED or REGRESSED since the last review).
Otherwise omit it entirely.
- Only raise NEW issues or issues whose status has changed.
- If nothing new to report, post exactly: "No new issues — see the
previous review comment."

Focus areas:
- Code: type safety, testing, telemetry, architecture consistency
- Docs: AGENTS.md context loading table, subsystem AGENTS.md,
Expand Down
84 changes: 75 additions & 9 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ env:
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
TEST_DEFAULT_MODEL: ${{ secrets.TEST_DEFAULT_MODEL }}

# Free up runners sooner: every push to a PR supersedes its in-flight PR run,
# so the old run (and its queued e2e jobs) are cancelled instead of consuming
# runner capacity that delays new commits.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:

lint:
Expand Down Expand Up @@ -271,9 +278,64 @@ jobs:
run: python tests/check_no_mock_in_integration.py
continue-on-error: true

e2e-smoke:
# L4a smoke: ~30s, PR-blocking. Runs in parallel with other test stages.
name: E2E smoke (L4a)
e2e-acp:
# L4a smoke: ACP protocol servers (stdio / streamable-http / WS).
# Runs in parallel with the opencode/misc e2e jobs; each job owns its own
# subprocess server cache instance, so wall-clock ≈ the slowest bucket
# instead of the serial sum.
name: E2E smoke (L4a · ACP)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: smoke
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
cache-suffix: py3.13
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --all-extras --no-group lint
- name: L4a smoke tests (ACP)
run: uv run pytest tests/e2e/test_acp*.py -m "e2e and not slow" --timeout=60 --tb=short --no-cov -q
- uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-acp-results
path: /tmp/pytest-*.txt
retention-days: 3

e2e-opencode:
name: E2E smoke (L4a · OpenCode)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: smoke
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: pyproject.toml
cache-suffix: py3.13
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --all-extras --no-group lint
- name: L4a smoke tests (OpenCode)
run: uv run pytest tests/e2e/test_opencode*.py -m "e2e and not slow" --timeout=60 --tb=short --no-cov -q
- uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-opencode-results
path: /tmp/pytest-*.txt
retention-days: 3

e2e-misc:
name: E2E smoke (L4a · AG-UI/APIs)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: smoke
Expand All @@ -289,19 +351,19 @@ jobs:
python-version: "3.13"
- name: Install dependencies
run: uv sync --all-extras --no-group lint
- name: L4a smoke tests
run: uv run pytest -m "e2e and not slow" --timeout=60 --tb=short --no-cov -q
- name: L4a smoke tests (AG-UI / APIs)
run: uv run pytest tests/e2e/test_agui*.py tests/e2e/test_openai*.py tests/e2e/test_user*.py tests/e2e/test_turn_split_e2e.py tests/e2e/test_team_mode_smoke.py -m "e2e and not slow" --timeout=60 --tb=short --no-cov -q
- uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-smoke-results
name: e2e-misc-results
path: /tmp/pytest-*.txt
retention-days: 3

report:
name: CI Report
runs-on: ubuntu-latest
needs: [lint, format, typecheck, import-linter, smoke, unit, integration, core, vcr, cassette-hygiene, e2e-smoke]
needs: [lint, format, typecheck, import-linter, smoke, unit, integration, core, vcr, cassette-hygiene, e2e-acp, e2e-opencode, e2e-misc]
if: always()
steps:
- name: Build summary table
Expand All @@ -320,7 +382,9 @@ jobs:
echo '| Core tests | ${{ needs.core.result }} |' >> $GITHUB_STEP_SUMMARY
echo '| VCR tests (L3 replay) | ${{ needs.vcr.result }} |' >> $GITHUB_STEP_SUMMARY
echo '| Cassette hygiene | ${{ needs.cassette-hygiene.result }} |' >> $GITHUB_STEP_SUMMARY
echo '| E2E smoke (L4a) | ${{ needs.e2e-smoke.result }} |' >> $GITHUB_STEP_SUMMARY
echo '| E2E smoke (ACP) | ${{ needs.e2e-acp.result }} |' >> $GITHUB_STEP_SUMMARY
echo '| E2E smoke (OpenCode) | ${{ needs.e2e-opencode.result }} |' >> $GITHUB_STEP_SUMMARY
echo '| E2E smoke (AG-UI/APIs) | ${{ needs.e2e-misc.result }} |' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
if [ '${{ needs.lint.result }}' = 'failure' ] || \
[ '${{ needs.format.result }}' = 'failure' ] || \
Expand All @@ -332,7 +396,9 @@ jobs:
[ '${{ needs.core.result }}' = 'failure' ] || \
[ '${{ needs.vcr.result }}' = 'failure' ] || \
[ '${{ needs.cassette-hygiene.result }}' = 'failure' ] || \
[ '${{ needs.e2e-smoke.result }}' = 'failure' ]; then
[ '${{ needs.e2e-acp.result }}' = 'failure' ] || \
[ '${{ needs.e2e-opencode.result }}' = 'failure' ] || \
[ '${{ needs.e2e-misc.result }}' = 'failure' ]; then
echo '### Some checks failed' >> $GITHUB_STEP_SUMMARY
echo 'Download the artifact for test details.' >> $GITHUB_STEP_SUMMARY
else
Expand Down
28 changes: 25 additions & 3 deletions .opencode/agents/review-lead.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,41 @@ single review comment. You do NOT edit files or run commands.
## Your Workflow

1. **Analyze the diff** — Review all changed files in the pull request.
2. **Categorize changes**:
2. **Read the PR thread context** — `<pull_request_comments>` and
`<pull_request_reviews>` contain ALL prior comments and reviews on this PR,
including reviews from earlier runs of this bot. Read them before delegating
so your specialists know what was already reported.
3. **Categorize changes**:
- **Code changes**: anything under `src/`, `tests/`, `scripts/`
- **Documentation changes**: `AGENTS.md`, `docs/`, `openspec/`
- **Config changes**: `pyproject.toml`, `.github/`, `mkdocs.yml`
3. **Delegate to specialists**:
4. **Delegate to specialists**:
- If there are code changes → invoke `@review-code` with the list of changed
code files.
- If there are documentation-relevant changes (new modules, new subsystems,
changed conventions, or actual doc file edits) → invoke `@review-docs` with
the list of changed files and a summary of what changed.
- If the PR is docs-only with no code changes, skip `@review-code`.
- If the PR is code-only with no documentation implications, skip `@review-docs`.
4. **Synthesize findings** — Combine specialist reports into a unified review.
5. **Synthesize findings** — Combine specialist reports into a unified review.

## De-duplication (MANDATORY)

This bot may review the same PR multiple times (reopened, ready_for_review,
manual triggers) and will produce a new comment on every run. Follow these rules
so findings are not repeated across runs:

- Before reporting a finding, check whether the same issue was already reported in
`<pull_request_comments>` or `<pull_request_reviews>`.
- Do NOT repeat a finding that already exists in the thread. Never restate an
existing comment.
- Re-mention a previously reported issue ONLY if its status changed (e.g. it was
FIXED or REGRESSED since the last review), and say what changed.
- Report only NEW issues, or previously-reported issues whose status changed.
- When passing a changed-files list to a specialist, also pass the already-reported
findings so the specialist avoids re-reporting them.
- If there is nothing new to report, post exactly: "No new issues — see the
previous review comment."

## Delegation Rules

Expand Down
4 changes: 4 additions & 0 deletions opencode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "review-lead"
}
3 changes: 3 additions & 0 deletions tests/e2e/test_opencode_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ async def test_get_health(subprocess_server: SubprocessServer) -> None:
strict=False,
)
@pytest.mark.known_bug
# Known to hang until the per-test timeout fires; cap it so CI doesn't burn the
# full 60s (the job-level timeout) just to reach the xfail.
@pytest.mark.timeout(10)
@pytest.mark.parametrize("subprocess_server", [_OPENCODE_PARAMS], indirect=True)
async def test_post_init(subprocess_server: SubprocessServer) -> None:
"""C10.2: POST /session/{session_id}/init, verify 200."""
Expand Down
Loading