diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 45d788c12..971fd9964 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -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 @@ -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 @@ -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 and + ). 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 or . + - 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, diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 0f9d87135..f4a0fa14c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -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: @@ -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 @@ -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 @@ -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' ] || \ @@ -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 diff --git a/.opencode/agents/review-lead.md b/.opencode/agents/review-lead.md index 29e6b6c21..e1b7d588e 100644 --- a/.opencode/agents/review-lead.md +++ b/.opencode/agents/review-lead.md @@ -28,11 +28,15 @@ 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** — `` and + `` 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, @@ -40,7 +44,25 @@ single review comment. You do NOT edit files or run commands. 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 + `` or ``. +- 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 diff --git a/opencode.json b/opencode.json new file mode 100644 index 000000000..0180eaaca --- /dev/null +++ b/opencode.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://opencode.ai/config.json", + "default_agent": "review-lead" +} \ No newline at end of file diff --git a/tests/e2e/test_opencode_misc.py b/tests/e2e/test_opencode_misc.py index 3bf810a02..c2c145f39 100644 --- a/tests/e2e/test_opencode_misc.py +++ b/tests/e2e/test_opencode_misc.py @@ -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."""