chore: import upstream snapshot with attribution
Harness Compat / harness compat (push) Failing after 0s
CI / test on 3.12 (standard) (push) Has been cancelled
CI / test on 3.13 (standard) (push) Has been cancelled
CI / test on 3.14 (standard) (push) Has been cancelled
CI / test on 3.10 (all-extras) (push) Has been cancelled
CI / test on 3.11 (all-extras) (push) Has been cancelled
CI / test on 3.12 (all-extras) (push) Has been cancelled
CI / test on 3.14 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.10 (pydantic-evals) (push) Has been cancelled
CI / test on 3.11 (pydantic-evals) (push) Has been cancelled
CI / test on 3.12 (pydantic-evals) (push) Has been cancelled
CI / deploy-docs-preview (push) Has been cancelled
CI / build release artifacts (push) Has been cancelled
CI / publish to PyPI (push) Has been cancelled
CI / Send tweet (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / mypy (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / test on 3.10 (standard) (push) Has been cancelled
CI / test on 3.11 (standard) (push) Has been cancelled
CI / test on 3.13 (all-extras) (push) Has been cancelled
CI / test on 3.14 (all-extras) (push) Has been cancelled
CI / test on 3.10 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.11 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.12 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-evals) (push) Has been cancelled
CI / test on 3.14 (pydantic-evals) (push) Has been cancelled
CI / test on 3.10 (lowest-versions) (push) Has been cancelled
CI / test on 3.11 (lowest-versions) (push) Has been cancelled
CI / test on 3.12 (lowest-versions) (push) Has been cancelled
CI / test on 3.13 (lowest-versions) (push) Has been cancelled
CI / test on 3.14 (lowest-versions) (push) Has been cancelled
CI / test examples on 3.11 (push) Has been cancelled
CI / test examples on 3.12 (push) Has been cancelled
CI / test examples on 3.13 (push) Has been cancelled
CI / test examples on 3.14 (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / check (push) Has been cancelled
CI / deploy-docs (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:27:52 +08:00
commit 9201ef759e
2096 changed files with 1232387 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# Agentic workflows (`gh-aw`)
The `pydantic-ai-*` workflows in this directory are [agentic workflows](https://github.com/githubnext/gh-aw) authored as human-editable `<name>.md` sources (frontmatter + prompt) that **compile** to a generated `<name>.lock.yml`. GitHub Actions runs the `.lock.yml`, never the `.md`.
- **Never hand-edit a `*.lock.yml`.** It is generated — the header says so. Manual edits are silently overwritten on the next recompile, and until then the running workflow diverges from its source.
- **After editing a workflow `*.md` source, recompile and commit the regenerated `*.lock.yml` in the same change** — a `*.md` edit without its recompiled lock is incomplete, and source and lock drift apart:
```
gh aw compile
```
- **Recompilation is required for anything the lock bakes in:** a source's frontmatter (`on:` triggers, `permissions`, `tools`, `safe-outputs`, jobs, path/`detect` filters) and its `imports:` shared fragments (`shared/*.md`) are inlined into the lock at compile time.
- **Exception — runtime-resolved prompts need no recompile.** Agent prompts under `shared/prompts/` are fetched at run time (via the `fetch-dynamic-prompt` action / a Logfire-managed variable), not baked into the lock, so editing one takes effect on the next run without recompiling.
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+137
View File
@@ -0,0 +1,137 @@
name: After CI
on:
# zizmor: ignore[dangerous-triggers] -- workflow_run is needed to access secrets after CI completes on fork PRs
workflow_run:
workflows: [CI]
types: [completed]
permissions: {}
jobs:
ci-duration-collect:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: "3.12"
- name: Collect CI duration telemetry
continue-on-error: true
run: uv run --no-project --with certifi --with logfire python .github/scripts/ci_duration.py collect --output ci-duration-record.json
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
CI_RUN_ID: ${{ github.event.workflow_run.id }}
CI_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
LOGFIRE_WRITE_TOKEN: ${{ secrets.LOGFIRE_WRITE_TOKEN }}
LOGFIRE_TOKEN: ${{ secrets.LOGFIRE_TOKEN }}
LOGFIRE_URL: ${{ vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
OTEL_EXPORTER_OTLP_HEADERS: Authorization=${{ secrets.LOGFIRE_TOKEN }}
- name: Store CI duration record
if: hashFiles('ci-duration-record.json') != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ci-duration-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
path: ci-duration-record.json
retention-days: 7
smokeshow:
runs-on: ubuntu-latest
permissions:
statuses: write
steps:
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: "3.12"
- uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
with:
workflow: ci.yml
name: "(diff-)?coverage-html.*"
name_is_regexp: true
commit: ${{ github.event.workflow_run.head_sha }}
allow_forks: true
workflow_conclusion: completed
if_no_artifact_found: warn
- run: uvx smokeshow upload coverage-html
if: hashFiles('coverage-html/*.html') != ''
env:
SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage}
SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95
SMOKESHOW_GITHUB_CONTEXT: coverage
SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }}
deploy-docs-preview:
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.pull_requests[0] != null
permissions:
pull-requests: write
environment:
name: deploy-docs-preview
steps:
- run: echo "$GITHUB_EVENT_JSON"
env:
GITHUB_EVENT_JSON: ${{ toJSON(github.event) }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- run: npm install
working-directory: docs-site
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: "3.12"
enable-cache: true
cache-suffix: deploy-docs-preview
- id: download-artifact
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
with:
workflow: ci.yml
name: site
path: site
commit: ${{ github.event.workflow_run.head_sha }}
allow_forks: true
workflow_conclusion: completed
if_no_artifact_found: warn
- uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
id: deploy
if: steps.download-artifact.outputs.found_artifact == 'true'
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: previews
workingDirectory: docs-site
command: >
deploy
--var GIT_COMMIT_SHA:${{ github.event.workflow_run.head_sha }}
--var GIT_BRANCH:${{ github.event.workflow_run.head_branch }}
- name: Set preview URL
run: uv run --no-project --with httpx .github/set_docs_pr_preview_url.py
if: steps.deploy.outcome == 'success'
env:
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
PULL_REQUEST_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
REF: ${{ github.event.workflow_run.head_sha }}
+582
View File
@@ -0,0 +1,582 @@
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
# | _ |/ _` |/ _ \ '_ \| __| |/ __|
# | | | | (_| | __/ | | | |_| | (__
# \_| |_/\__, |\___|_| |_|\__|_|\___|
# __/ |
# _ _ |___/
# | | | | / _| |
# | | | | ___ _ __ _ __| |_| | _____ ____
# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
#
# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.74.8). DO NOT EDIT.
#
# To regenerate this workflow, run:
# gh aw compile
# Not all edits will cause changes to this file.
#
# For more information: https://github.github.com/gh-aw/introduction/overview/
#
# Alternative regeneration methods:
# make recompile
#
# Or use the gh-aw CLI directly:
# ./gh-aw compile --validate --verbose
#
# The workflow is generated when any workflow uses the 'expires' field
# in create-discussions, create-issues, or create-pull-request safe-outputs configuration.
# Schedule frequency is automatically determined by the shortest expiration time.
#
name: Agentic Maintenance
on:
schedule:
- cron: "37 0 * * *" # Daily (based on minimum expires: 7 days)
workflow_dispatch:
inputs:
operation:
description: 'Optional maintenance operation to run'
required: false
type: choice
default: ''
options:
- ''
- 'disable'
- 'enable'
- 'update'
- 'upgrade'
- 'safe_outputs'
- 'create_labels'
- 'activity_report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
- 'update_pull_request_branches'
- 'validate'
- 'forecast'
run_url:
description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
required: false
type: string
default: ''
workflow_call:
inputs:
operation:
description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, update_pull_request_branches, validate, forecast)'
required: false
type: string
default: ''
run_url:
description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
required: false
type: string
default: ''
outputs:
operation_completed:
description: 'The maintenance operation that was completed (empty when none ran or a scheduled job ran)'
value: ${{ jobs.run_operation.outputs.operation || inputs.operation }}
applied_run_url:
description: 'The run URL that safe outputs were applied from'
value: ${{ jobs.apply_safe_outputs.outputs.run_url }}
permissions: {}
jobs:
close-expired-entities:
if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
runs-on: ubuntu-slim
permissions:
discussions: write
issues: write
pull-requests: write
steps:
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Close expired discussions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_discussions.cjs');
await main();
- name: Close expired issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_issues.cjs');
await main();
- name: Close expired pull requests
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/close_expired_pull_requests.cjs');
await main();
cleanup-cache-memory:
if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories') }}
runs-on: ubuntu-slim
permissions:
actions: write
steps:
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Cleanup outdated cache-memory entries
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/cleanup_cache_memory.cjs');
await main();
run_operation:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'update_pull_request_branches' && inputs.operation != 'validate' && inputs.operation != 'forecast' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
actions: write
contents: write
pull-requests: write
outputs:
operation: ${{ steps.record.outputs.operation }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Install gh-aw
uses: github/gh-aw-actions/setup-cli@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
version: v0.74.8
- name: Run operation
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_AW_OPERATION: ${{ inputs.operation }}
GH_AW_CMD_PREFIX: gh aw
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/run_operation_update_upgrade.cjs');
await main();
- name: Record outputs
id: record
run: echo "operation=${{ inputs.operation }}" >> "$GITHUB_OUTPUT"
update_pull_request_branches:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'update_pull_request_branches' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
contents: write
pull-requests: write
steps:
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Update pull request branches
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/update_pull_request_branches.cjs');
await main();
apply_safe_outputs:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'safe_outputs' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
actions: read
contents: write
discussions: write
issues: write
pull-requests: write
outputs:
run_url: ${{ steps.record.outputs.run_url }}
steps:
- name: Checkout actions folder
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
sparse-checkout: |
actions
persist-credentials: false
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Apply Safe Outputs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_AW_RUN_URL: ${{ inputs.run_url }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/apply_safe_outputs_replay.cjs');
await main();
- name: Record outputs
id: record
run: echo "run_url=${{ inputs.run_url }}" >> "$GITHUB_OUTPUT"
create_labels:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Install gh-aw
uses: github/gh-aw-actions/setup-cli@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
version: v0.74.8
- name: Create missing labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_CMD_PREFIX: gh aw
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/create_labels.cjs');
await main();
activity_report:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
timeout-minutes: 120
permissions:
actions: read
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Install gh-aw
uses: github/gh-aw-actions/setup-cli@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
version: v0.74.8
- name: Restore activity report logs cache
id: activity_report_logs_cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ./.cache/gh-aw/activity-report-logs
key: ${{ runner.os }}-activity-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-activity-report-logs-${{ github.repository }}-
${{ runner.os }}-activity-report-logs-
- name: Download activity report logs
timeout-minutes: 20
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_AW_CMD_PREFIX: gh aw
run: |
${GH_AW_CMD_PREFIX} logs \
--repo "${{ github.repository }}" \
--start-date -1w \
--count 100 \
--output ./.cache/gh-aw/activity-report-logs \
--format markdown \
> ./.cache/gh-aw/activity-report-logs/report.md
- name: Save activity report logs cache
if: ${{ always() }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ./.cache/gh-aw/activity-report-logs
key: ${{ steps.activity_report_logs_cache.outputs.cache-primary-key }}
- name: Generate activity report issue
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('node:fs');
const reportPath = './.cache/gh-aw/activity-report-logs/report.md';
if (!fs.existsSync(reportPath)) {
core.warning('Activity report markdown not found at ' + reportPath + '; skipping issue creation.');
return;
}
let reportBody = '';
try {
reportBody = fs.readFileSync(reportPath, 'utf8').trim();
} catch (error) {
core.warning('Failed to read activity report markdown at ' + reportPath + ': ' + error.message);
return;
}
if (!reportBody) {
core.warning('Activity report markdown is empty at ' + reportPath + '; skipping issue creation.');
return;
}
const repoSlug = context.repo.owner + '/' + context.repo.repo;
const body = [
'### Agentic workflow activity report',
'',
'Repository: ' + repoSlug,
'Generated at: ' + new Date().toISOString(),
'',
reportBody,
].join('\n');
const createdIssue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '[aw] agentic status report',
body,
labels: ['agentic-workflows'],
});
core.info('Created issue #' + createdIssue.data.number + ': ' + createdIssue.data.html_url);
forecast_report:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'forecast' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
timeout-minutes: 60
permissions:
actions: read
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Install gh-aw
uses: github/gh-aw-actions/setup-cli@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
version: v0.74.8
- name: Restore forecast report logs cache
id: forecast_report_logs_cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .github/aw/logs
key: ${{ runner.os }}-forecast-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-forecast-report-logs-${{ github.repository }}-
${{ runner.os }}-forecast-report-logs-
- name: Generate forecast report
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_AW_CMD_PREFIX: gh aw
run: |
mkdir -p ./.cache/gh-aw/forecast
${GH_AW_CMD_PREFIX} logs --repo "${{ github.repository }}" --start-date -30d --count 1500 > /dev/null
if ! compgen -G ".github/aw/logs/run-*/run_summary.json" > /dev/null; then
echo "::error::Missing run summary cache in .github/aw/logs after gh aw logs warm-up; cannot run forecast."
exit 1
fi
${GH_AW_CMD_PREFIX} forecast --repo "${{ github.repository }}" --json 2> >(grep -Fv "forecast is an experimental command and may change without notice" >&2) > ./.cache/gh-aw/forecast/report.json
- name: Save forecast report logs cache
if: ${{ always() }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .github/aw/logs
key: ${{ steps.forecast_report_logs_cache.outputs.cache-primary-key }}
- name: Generate forecast issue
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/create_forecast_issue.cjs');
await main();
close_agentic_workflows_issues:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
issues: write
steps:
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Close no-repro agentic-workflows issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/close_agentic_workflows_issues.cjs');
await main();
validate_workflows:
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'validate' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Scripts
uses: github/gh-aw-actions/setup@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
destination: ${{ runner.temp }}/gh-aw/actions
- name: Check admin/maintainer permissions
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();
- name: Install gh-aw
uses: github/gh-aw-actions/setup-cli@efa55847f72aadb03490d955263ff911bf758700 # v0.74.8
with:
version: v0.74.8
- name: Validate workflows and file issue on findings
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GH_AW_CMD_PREFIX: gh aw
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/run_validate_workflows.cjs');
await main();
+147
View File
@@ -0,0 +1,147 @@
name: '@claude'
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions: {}
env:
UV_PYTHON: 3.13
UV_FROZEN: "1"
jobs:
get-pr-info:
permissions:
contents: read
if: |
(
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
) && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'),
github.event.comment.author_association ||
github.event.review.author_association ||
github.event.issue.author_association)
runs-on: ubuntu-latest
outputs:
is_fork: ${{ steps.pr-info.outputs.is_fork }}
pr_head_repo: ${{ steps.pr-info.outputs.pr_head_repo }}
pr_head_sha: ${{ steps.pr-info.outputs.pr_head_sha }}
pr_base_sha: ${{ steps.pr-info.outputs.pr_base_sha }}
steps:
- name: Get PR info
if: github.event.issue.pull_request || github.event.pull_request
id: pr-info
run: |
PR_NUMBER=${{ github.event.pull_request.number || github.event.issue.number }}
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER})
echo "pr_head_repo=$(echo "$PR_DATA" | jq -r '.head.repo.full_name')" >> $GITHUB_OUTPUT
echo "pr_head_sha=$(echo "$PR_DATA" | jq -r '.head.sha')" >> $GITHUB_OUTPUT
echo "pr_base_sha=$(echo "$PR_DATA" | jq -r '.base.sha')" >> $GITHUB_OUTPUT
echo "is_fork=$(echo "$PR_DATA" | jq -r '.head.repo.fork')" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
at-claude:
needs: get-pr-info
if: needs.get-pr-info.outputs.is_fork != 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-suffix: claude-code
- run: uv tool install pre-commit
- run: make install
- name: Install bubblewrap
run: sudo apt-get install -y bubblewrap
- uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1.0.157
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr checks:*),Bash(gh pr list:*),Bash(gh pr create:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh run view:*),Bash(gh run list:*),Bash(git log:*),Bash(git diff:*),Bash(git grep:*),Bash(git show:*),Bash(git status:*),Bash(git add:*),Bash(git checkout:*),Bash(git commit:*),Bash(git push:*),Bash(rg:*),Bash(ls:*),Bash(tree:*),Bash(grep:*),Bash(uv run:*),Bash(make:*)"
at-claude-fork:
needs: get-pr-info
if: needs.get-pr-info.outputs.is_fork == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
# Least privilege: on forks Claude may only read code and comment (see the
# restricted --allowedTools below — no push / pr create), so this job runs
# with read-only `contents`. It never gets write access to repository code.
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Checkout fork repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ needs.get-pr-info.outputs.pr_head_repo }}
ref: ${{ needs.get-pr-info.outputs.pr_head_sha }}
fetch-depth: 1
persist-credentials: false
- name: Check for modified config files
# Compare the exact immutable SHAs captured in get-pr-info (and checked
# out above) rather than the live PR head. Using `gh pr diff` would read
# whatever the PR points at now, so a force-push after get-pr-info could
# slip a modified CLAUDE.md/AGENTS.md past this guard while different
# code is what actually runs (TOCTOU). Pinning to BASE_SHA...HEAD_SHA
# ties the check to the code Claude will see.
#
# Use the diff media type, not the compare JSON: the JSON `.files` array
# is capped at 300 entries (and `--paginate` only pages commits, not
# files), so a >300-file PR could hide an agent-config edit. The diff
# emits a `diff --git a/<old> b/<new>` header for every changed file,
# including renames, so matching those lines cannot miss a path.
run: |
CHANGED=$(gh api "repos/${{ github.repository }}/compare/${BASE_SHA}...${HEAD_SHA}" -H 'Accept: application/vnd.github.v3.diff' | grep '^diff --git ')
if echo "$CHANGED" | grep -qiE '/(AGENTS\.md|CLAUDE\.md)( |$)|/\.claude/'; then
echo "::error::PR modifies agent config files (AGENTS.md, CLAUDE.md, or .claude/). Skipping for security."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_SHA: ${{ needs.get-pr-info.outputs.pr_base_sha }}
HEAD_SHA: ${{ needs.get-pr-info.outputs.pr_head_sha }}
- name: Install bubblewrap
run: sudo apt-get install -y bubblewrap
- uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1.0.157
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr checks:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh run view:*),Bash(gh run list:*),Bash(git log:*),Bash(git diff:*),Bash(git grep:*),Bash(git show:*),Bash(git status:*),Bash(rg:*),Bash(ls:*),Bash(tree:*),Bash(grep:*)"
+375
View File
@@ -0,0 +1,375 @@
name: PR Bots
on:
# zizmor: ignore[dangerous-triggers] -- pull_request_target is needed for fork PRs; inputs are validated before use
pull_request_target:
types: [opened, synchronize, labeled]
permissions: {}
concurrency:
group: pr-bots-${{ github.event.pull_request.number }}
cancel-in-progress: ${{ github.event.action == 'synchronize' }}
jobs:
size-label:
name: Size Label
if: github.event.action != 'labeled'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Calculate PR size and set label
run: |
# Fetch file changes for this PR
FILES=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files --paginate | jq -s 'add')
# Calculate lines by category (excluding uv.lock and cassettes)
CODE=$(echo "$FILES" | jq '[.[] | select(
(.filename | (startswith("tests/") or startswith("docs/") or endswith(".md")) | not) and
(.filename != "uv.lock") and
(.filename | contains("/cassettes/") | not)
) | .additions + .deletions] | add // 0')
DOCS=$(echo "$FILES" | jq '[.[] | select(
(.filename | (startswith("docs/") or endswith(".md"))) and
(.filename != "uv.lock") and
(.filename | contains("/cassettes/") | not)
) | .additions + .deletions] | add // 0')
TESTS=$(echo "$FILES" | jq '[.[] | select(
(.filename | startswith("tests/")) and
(.filename | contains("/cassettes/") | not) and
(.filename | endswith(".md") | not)
) | .additions + .deletions] | add // 0')
# Calculate weighted score: code + 50% docs + 50% tests
SCORE=$((CODE + DOCS / 2 + TESTS / 2))
echo "Code: $CODE, Docs: $DOCS, Tests: $TESTS"
echo "Weighted score: $SCORE"
# Determine size label based on cutoffs
if [ $SCORE -le 100 ]; then
SIZE="size: S"
elif [ $SCORE -le 500 ]; then
SIZE="size: M"
elif [ $SCORE -le 1500 ]; then
SIZE="size: L"
else
SIZE="size: XL"
fi
echo "Size: $SIZE"
# Remove any existing size labels (except the one we're setting) via API
for label in "size: S" "size: M" "size: L" "size: XL"; do
if [ "$label" != "$SIZE" ]; then
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/${label}" --method DELETE 2>/dev/null || true
fi
done
# Add the new size label via API
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" --method POST -f "labels[]=$SIZE"
echo "Set label: $SIZE (score: $SCORE)"
env:
GH_TOKEN: ${{ github.token }}
# Security: The classify job runs the LLM with READ-ONLY permissions and no label API access.
# The LLM's output is validated against an allowlist before the apply job takes any write action.
# This prevents prompt injection from adding arbitrary labels (e.g. 'auto-review' to trigger the review job).
category-classify:
name: Category Classify
if: github.event.action != 'labeled'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
outputs:
category: ${{ steps.extract.outputs.category }}
skip: ${{ steps.check-label.outputs.has_label }}
steps:
- name: Check for modified config files
if: github.event.pull_request.head.repo.fork
run: |
CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only --repo ${{ github.repository }})
if echo "$CHANGED" | grep -qiE '(^|/)AGENTS\.md$|(^|/)CLAUDE\.md$|(^|/)\.claude/'; then
echo "::error::PR modifies agent config files (AGENTS.md, CLAUDE.md, or .claude/). Skipping auto-labeling for security."
exit 1
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Check if category label already exists
id: check-label
run: |
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json labels --jq '.labels[].name')
CATEGORY_LABELS=("bug" "feature" "docs" "chore" "dependency")
for label in "${CATEGORY_LABELS[@]}"; do
if echo "$LABELS" | grep -q "^${label}$"; then
echo "has_label=true" >> $GITHUB_OUTPUT
echo "PR already has category label: $label"
exit 0
fi
done
echo "has_label=false" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
- name: Checkout repository
if: steps.check-label.outputs.has_label == 'false'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install bubblewrap
if: steps.check-label.outputs.has_label == 'false'
run: sudo apt-get install -y bubblewrap
- name: Classify PR with Claude Code
if: steps.check-label.outputs.has_label == 'false'
id: classify
uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1.0.157
env:
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_CODE_BASE_URL }}
with:
anthropic_api_key: ${{ secrets.CLAUDE_CODE_API_KEY || secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
allowed_non_write_users: "*"
allowed_bots: "pydanty"
claude_args: |
--allowedTools "Bash(gh pr view:*),Bash(gh pr diff:*)"
--json-schema '{"type":"object","properties":{"category":{"type":"string"}},"required":["category"]}'
prompt: |
Classify PR #${{ github.event.pull_request.number }} in ${{ github.repository }}.
Run `gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }}` for the title and description.
Run `gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only` for the changed files.
Categories:
- bug: Fixes broken behavior
- feature: New functionality
- docs: Documentation-only (no code changes)
- chore: CI, refactoring, dev dependencies, tests-only
- dependency: Production dependency updates
When both code and docs change, prefer `feature` or `bug` over `docs`.
If pyproject.toml files changed, run `gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }}` to see the full diff and distinguish production deps (`dependency`) from dev-only deps in `[dependency-groups]` (`chore`).
Return your classification as JSON with a "category" field set to one of: bug, feature, docs, chore, dependency.
- name: Extract and validate category
if: steps.check-label.outputs.has_label == 'false'
id: extract
run: |
CATEGORY=$(echo "$STRUCTURED_OUTPUT" | jq -r .category | tr -d '[:space:]')
ALLOWED="bug feature docs chore dependency"
if ! echo "$ALLOWED" | grep -Fqw "$CATEGORY"; then
echo "::error::Invalid category '$CATEGORY' from Claude — must be one of: $ALLOWED"
exit 1
fi
echo "category=$CATEGORY" >> $GITHUB_OUTPUT
echo "Classified as: $CATEGORY"
env:
STRUCTURED_OUTPUT: ${{ steps.classify.outputs.structured_output }}
category-apply:
name: Category Apply
needs: category-classify
if: needs.category-classify.outputs.skip != 'true' && needs.category-classify.outputs.category != ''
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Validate and apply category label
run: |
CATEGORY="${NEEDS_CATEGORY_CLASSIFY_OUTPUTS_CATEGORY}"
ALLOWED="bug feature docs chore dependency"
if ! echo "$ALLOWED" | grep -Fqw "$CATEGORY"; then
echo "::error::Invalid category '$CATEGORY' — must be one of: $ALLOWED"
exit 1
fi
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \
--method POST -f "labels[]=$CATEGORY"
echo "Applied label: $CATEGORY"
env:
GH_TOKEN: ${{ github.token }}
NEEDS_CATEGORY_CLASSIFY_OUTPUTS_CATEGORY: ${{ needs.category-classify.outputs.category }}
review:
name: Review
needs: [size-label, category-apply]
if: >-
!failure() && !cancelled() &&
github.event.action == 'labeled' && github.event.label.name == 'auto-review'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
issues: write
pull-requests: write
actions: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
persist-credentials: false
- name: Check for modified config files
if: github.event.pull_request.head.repo.fork
run: |
CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only --repo ${{ github.repository }})
if echo "$CHANGED" | grep -qiE '(^|/)AGENTS\.md$|(^|/)CLAUDE\.md$|(^|/)\.claude/'; then
echo "::error::PR modifies agent config files (AGENTS.md, CLAUDE.md, or .claude/). Skipping auto-review for security."
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Select review model
id: model
run: |
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json labels --jq '.labels[].name')
echo "Labels: $LABELS"
CATEGORY=$(echo "$LABELS" | grep -E '^(bug|feature|docs|chore|dependency)$' | head -1)
SIZE=$(echo "$LABELS" | grep -E '^size: ' | head -1)
# Default to Opus for large/complex PRs
MODEL="claude-opus-4-6[1m]"
# Disabled for now because Sonnet reviews have been disappointing
# # Use Sonnet for docs, dependency, chore, or small/medium PRs
# if [ "$CATEGORY" = "docs" ] || [ "$CATEGORY" = "dependency" ] || [ "$CATEGORY" = "chore" ] || [ "$SIZE" = "size: S" ] || [ "$SIZE" = "size: M" ]; then
# MODEL="claude-sonnet-4-5[1m]"
# fi
echo "model=$MODEL" >> $GITHUB_OUTPUT
echo "Selected model: $MODEL (category: $CATEGORY, size: $SIZE)"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Gather PR context
run: |
# Use the script from the base repo (main branch), not the fork
gh api "repos/${REPO}/contents/scripts/gather-review-context.sh?ref=${BASE_REF}" --jq .content | base64 -d > /tmp/gather-review-context.sh
bash /tmp/gather-review-context.sh "$PR_NUMBER" "$REPO"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Install bubblewrap
run: sudo apt-get install -y bubblewrap
- uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1.0.157
env:
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_CODE_BASE_URL }}
with:
anthropic_api_key: ${{ secrets.CLAUDE_CODE_API_KEY || secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
display_report: 'true'
additional_permissions: |
actions: read
claude_args: |
--model ${{ steps.model.outputs.model }}
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr checks:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh run view:*),Bash(gh run list:*),Bash(gh api repos/${{ github.repository }}/pulls/comments/:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews:*),Bash(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments:*),Bash(git log:*),Bash(git diff:*),Bash(git grep:*),Bash(git show:*),Bash(git status:*),Bash(jq:*),Bash(cat:*),Bash(rg:*),Bash(ls:*),Bash(tree:*),Bash(grep:*),WebSearch,WebFetch"
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
PR AUTHOR: ${{ github.event.pull_request.user.login }} (${{ github.event.pull_request.author_association }})
Review this pull request. The PR branch is already checked out in the current working directory and the `CLAUDE.md` at the root (symlinked to `AGENTS.md`) is already loaded in your system prompt.
# What to look for
- If the PR should not have been created yet (e.g. no issue, insufficiently defined scope, not ready for implementation, duplicate of existing open PR), just leave a comment informing the user and maintainer of this and don't bother doing a thorough review.
- Any change that does not align with the project's standards, philosophy, or requirements for every contribution as stated in `AGENTS.md` (symlinked from `CLAUDE.md`).
- Any change that does not match maintainer guidance in the issue or earlier PR comments on what an acceptable solution would look like.
- Any change or design decision or tradeoff (in both behavior and API) that needs explicit consideration, discussion, or maintainer awareness and approval.
- Any line of code that violates the concrete guidelines/rules laid out in the relevant `AGENTS.md` file(s): the top-level guidelines apply to all changes, while directory-specific guidelines affect only the changes in that directory.
- Anything else that the responsibilities you are assigned in `AGENTS.md` suggest that you should be calling out: use your best judgment.
Generally, the priority in terms of "crucial to get right" and "what to focus on first in a new PR" is public API > concepts and behavior > documentation > tests > code style.
If the PR has high level problems that will likely require significant changes at lower levels, hold off on looking for or commenting on lower level problems until the higher level problems are addressed, so that the PR author (and your context window) don't get overwhelmed.
Note that while another agent (Devin) is responsible for thoroughly reviewing the implementation for bugs, security issues, and edge cases,
_you_ are responsible for catching every violation of the repository's standards and guidelines listed in the `AGENTS.md` and `agent_docs/*.md` files.
Do not focus exclusively on high-level concerns: by the time the author has addressed every comment you've left over multiple rounds of review, the PR should be ready to merge.
# Gathering context
Before doing anything else, read ALL of the following pre-gathered context files in a single parallel tool call:
- `.github/.review-context/pr-details.json` — PR title, body, author, branch info, labels, state, review decision, timestamps
- `.github/.review-context/pr-comments.txt` — existing top-level PR comments
- `.github/.review-context/review-comments.txt` — existing inline review comments (resolved+outdated threads and threads predating the last auto-review are collapsed to one-liners with comment IDs so you can fetch full details if needed)
- `.github/.review-context/related-issues.txt` — linked issues and their comments
- `.github/.review-context/changed-files.txt` — changed files with per-file addition/deletion counts (tab-separated; non-generated files include a third column with the path to their per-file diff)
- `.github/.review-context/agents-md.txt` — directory-specific `AGENTS.md` files for changed directories
- `agent_docs/index.md` - repo-wide coding guidelines
The diff is split into per-file diffs under `.github/.review-context/diff/` (excluding `uv.lock` and cassettes), that you can read on demand and in parallel.
The diffs include function-level context (`git diff -W`), so you can see the full function/method being modified without needing to read the source file separately.
Each commentable line in the diff is prefixed with its source line number: `NL:<number>` for new or context lines, `OL:<number>` for deleted lines.
For newly added files, the diff contains the complete file contents — do not re-read these from disk.
The diff file paths are listed in the third column of `changed-files.txt`.
The pre-gathered diffs are the source of truth for what this PR changes. Do not re-fetch diffs or file lists using `gh pr diff` or `gh api`.
When you need code context beyond what the diffs provide, use the `Read` tool on the checked-out source files.
Use the `gh` CLI only when you need additional information not already in these files (e.g. to read other referenced PRs or issues, check CI status, or read files excluded from the gathered diff).
Use specific `gh` subcommands (`gh pr view`, `gh issue view`, `gh run view`, etc.) rather than `gh api` for most queries.
`gh api` is scoped to comment and review endpoints on this PR only:
- `gh api repos/${{ github.repository }}/pulls/comments/<id>` — individual review comment by ID
- `gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments` — list review comments
- `gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews` — list reviews
- `gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments` — list issue comments
Be careful about loading large diffs if you're unlikely to need them yet, like massive test files when there's plenty of more interesting code to comment on first, as you don't want to blow your context window too early.
You will usually want to read all the "core implementation" and docs diffs in one go, though, so you have the full context of the PR as you identify problems, instead of going file by file.
# Posting comments
While gathering context and learning about the PR, keep track of problems/points of discussion as you find them, and wait to post comments until the end,
as the comments you write will be better, less duplicative and more focused on the changes that really matter if you have the full set of problems as context.
For each identified issue that is determined to be worth a new comment, use `mcp__github_inline_comment__create_inline_comment` to attach the feedback to a specific line of code.
- Only lines with an `NL:` or `OL:` prefix in the diff are commentable. For OL lines, use `side: LEFT`.
- Include the reasoning, but don't quote specific rules from the `AGENTS.md` files.
- Include a concrete suggestion if appropriate (but to not use ` ```suggestion ` blocks as they can render incorrectly when the line numbers are off)
- Include a ping to the maintainer (`@DouweM`) on any change that requires maintainer input before the PR author can move forward.
- If the same issue shows up in multiple places, post a comment on each instance but have later comments refer to the first comment using a link.
- Use `gh pr comment` only for important feedback that doesn't relate to a specific line or file, not for a summary of feedback you've already posted inline.
Your comments should be:
- actionable: they should request a change, flag a concern that needs discussion, and/or suggest an improvement; don't comment on positive aspects of the PR like "excellent design choices".
- concise and to the point: don't use unnecessary emojis, lists, or subheadings, but do link to code if appropriate; 1 to 3 paragraphs are pretty much always enough.
- friendly without being sycophantic: use the tone and language of a helpful and encouraging project maintainer, but no need to compliment the author on positive aspects of the PR or point out changes that are good.
- non-repetitive: don't repeat things pointed out in earlier review comments, unless it looks like they'll be forgotten if you don't point them out; e.g. when they're marked as resolved/outdated but the problem persists without a satisfactory resolution (like a maintainer comment saying the comment does not need to be addressed).
You are meant to be helpful to the contributor and the maintainer, so your comments should never add noise to the conversation:
- Do not post a final summary comment; inline comments are sufficient.
- Do not comment on lines that do not need improvement, maintainer awareness, or discussion; comments pointing out a good choice are just noise.
- Do not post multiple comments for the same exact issue unless it shows up in different places.
It bears repeating that you are the first line of defense against low-quality contributions and maintainer headaches, and you have a big role in ensuring that every contribution to this project meets or exceeds the high standards that the Pydantic brand is known and loved for.
- name: Remove auto-review label
if: always()
run: gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/auto-review" --method DELETE 2>/dev/null || true
env:
GH_TOKEN: ${{ github.token }}
+55
View File
@@ -0,0 +1,55 @@
name: CI Duration Report
# Label-triggered CI duration report. Adding `trigger:ci-duration-report` to a
# PR compares that PR's latest CI run against recent successful `main` and PR
# runs, then updates one sticky comment on the PR. This workflow never checks
# out or executes PR head code.
on:
# zizmor: ignore[dangerous-triggers] -- pull_request_target is required so fork PRs can
# receive comments from a base-repo token. The workflow is gated on a maintainer-applied
# label and only reads GitHub Actions metadata.
pull_request_target:
types: [labeled]
permissions: {}
concurrency:
group: ci-duration-report-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
report:
if: github.event.label.name == 'trigger:ci-duration-report'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: read
issues: write
pull-requests: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: "3.12"
- name: Create or update CI duration report
run: uv run --no-project --with certifi python .github/scripts/ci_duration.py report --poll-seconds 600
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
- name: Remove trigger label
if: always()
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: gh api -X DELETE "repos/${REPO}/issues/${PR_NUMBER}/labels/trigger:ci-duration-report" || true
+522
View File
@@ -0,0 +1,522 @@
name: CI
on:
push:
branches:
- main
tags:
- "**"
pull_request: {}
env:
COLUMNS: 150
UV_PYTHON: 3.12
UV_FROZEN: "1"
permissions:
contents: read
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: "3.13"
enable-cache: true
cache-suffix: lint
- name: Install dependencies
run: uv sync --all-extras --all-packages --group lint
# pyright typechecks the gh-aw shim (.github/scripts/pydantic_ai_gh_aw_shim),
# which imports pydantic-ai-harness. The harness depends on pydantic-ai-slim,
# whose name is shadowed by the workspace member, so keeping it in the
# universal lock breaks the lowest-direct re-resolution. Install it into the
# synced venv out-of-band instead (--no-deps: pydantic-ai-slim is already
# present from the workspace). Keep the pin in sync with the runner's
# (.github/scripts/pydantic-ai-runner).
- run: uv pip install --no-deps "pydantic-ai-harness>=0.4.0"
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
with:
extra_args: --all-files --verbose
env:
SKIP: no-commit-to-branch
# pre-commit hooks shell out via `uv run`; without this they would
# re-sync the venv and drop the out-of-band harness install above.
UV_NO_SYNC: "1"
- run: uv build --all-packages
- run: ls -lh dist/
# mypy and lint are a bit slower than other jobs, so we run them separately
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-suffix: mypy
- name: Install dependencies
run: uv sync --no-dev --group lint
- run: make typecheck-mypy
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-suffix: docs
- run: uv sync --group docs
- run: make docs
- run: tree -sh site
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- run: npm install
working-directory: docs-site
- run: npm run typecheck
working-directory: docs-site
- name: Store docs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: site
path: site
# check all docs images are tinified, You'll need an API key from https://tinify.com/ to fix this if it fails
- run: uvx tinicly docs --check
test:
name: test on ${{ matrix.python-version }} (${{ matrix.install.name }})
# Use Ubicloud 4-core runners for all-extras (the slowest variant) when run by maintainers or opted in via 'ci:fast' label
# Use 'ci:slow' label to force standard GitHub runners (e.g. during Ubicloud outages)
runs-on: >-
${{
!contains(github.event.pull_request.labels.*.name, 'ci:slow')
&& matrix.install.name == 'all-extras'
&& (
github.event.pull_request.head.repo.full_name == github.repository
|| contains(github.event.pull_request.labels.*.name, 'ci:fast')
)
&& 'ubicloud-premium-4'
|| 'ubuntu-latest'
}}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
install:
- name: pydantic-ai-slim
command: "--package pydantic-ai-slim"
- name: pydantic-evals
command: "--package pydantic-evals"
- name: standard
command: ""
- name: all-extras
command: "--all-extras"
env:
CI: true
COVERAGE_PROCESS_START: ./pyproject.toml
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: ${{ matrix.install.name }}
- run: mkdir .coverage
- run: uv sync --only-dev
- name: cache HuggingFace models
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/huggingface
key: hf-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
restore-keys: |
hf-${{ runner.os }}-
# Warm the HF cache out-of-band (retried, non-fatal) so the sentence-transformers
# test model is on disk when the ST tests load it offline (see the
# `stsb_bert_tiny_model` fixture). On a sustained HF outage the cache stays
# cold and those tests skip instead of erroring the whole job.
- name: pre-download sentence-transformers test model
if: matrix.install.name == 'all-extras'
continue-on-error: true
run: |
for i in 1 2 3; do
if uv run --all-extras python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers-testing/stsb-bert-tiny-safetensors')"; then
exit 0
fi
echo "HF model download attempt $i failed; retrying in 15s..."
sleep 15
done
echo "sentence-transformers test model unavailable after retries; ST tests will skip"
exit 1
# `-n logical`, not `-n auto`: xdist `auto` counts *physical* cores, which
# under-subscribes Ubicloud's hyperthreaded vCPUs (premium-4 -> 2 workers
# instead of 4). `logical` uses all vCPUs; ~39% faster on premium-4, no-op
# on ubuntu-latest (already 4). See PR benchmark.
- run: uv run ${{ matrix.install.command }} coverage run -m pytest --durations=100 -n logical --dist=loadgroup
env:
COVERAGE_FILE: .coverage/.coverage.${{ matrix.python-version }}-${{ matrix.install.name }}
- name: store coverage files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.install.name }}
path: .coverage
include-hidden-files: true
test-lowest-versions:
name: test on ${{ matrix.python-version }} (lowest-versions)
# Use Ubicloud 4-core runners for maintainers or opted in via 'ci:fast' label
# Use 'ci:slow' label to force standard GitHub runners (e.g. during Ubicloud outages)
runs-on: >-
${{
!contains(github.event.pull_request.labels.*.name, 'ci:slow')
&& (
github.event.pull_request.head.repo.full_name == github.repository
|| contains(github.event.pull_request.labels.*.name, 'ci:fast')
)
&& 'ubicloud-premium-4'
|| 'ubuntu-latest'
}}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
env:
CI: true
COVERAGE_PROCESS_START: ./pyproject.toml
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: lowest-versions
- run: mkdir .coverage
- name: cache HuggingFace models
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/huggingface
key: hf-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}
restore-keys: |
hf-${{ runner.os }}-
- run: uv sync --all-extras --resolution lowest-direct
env:
UV_FROZEN: "0"
# Warm the HF cache out-of-band (see the `test` job) so the ST tests load
# the model offline from disk instead of revalidating it against HF Hub.
- name: pre-download sentence-transformers test model
continue-on-error: true
run: |
for i in 1 2 3; do
if uv run --no-sync python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers-testing/stsb-bert-tiny-safetensors')"; then
exit 0
fi
echo "HF model download attempt $i failed; retrying in 15s..."
sleep 15
done
echo "sentence-transformers test model unavailable after retries; ST tests will skip"
exit 1
# `-n logical` (see note on the `test` job): use all vCPUs on Ubicloud.
- run: uv run --no-sync coverage run -m pytest --durations=100 -n logical --dist=loadgroup
env:
COVERAGE_FILE: .coverage/.coverage.${{matrix.python-version}}-lowest-versions
- name: store coverage files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-${{ matrix.python-version }}-lowest-versions
path: .coverage
include-hidden-files: true
test-examples:
name: test examples on ${{ matrix.python-version }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
env:
CI: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: examples
- run: uv run --all-extras python tests/import_examples.py
coverage:
runs-on: ubuntu-latest
needs: [test, test-lowest-versions]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# needed for diff-cover
fetch-depth: 0
persist-credentials: false
- name: get coverage files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
merge-multiple: true
path: .coverage
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-suffix: dev
- run: uv sync --group dev
- run: uv run coverage combine
- run: uv run coverage report
- run: uv run strict-no-cover
env:
COVERAGE_FILE: .coverage/.coverage
- run: uv run coverage html --show-contexts --title "Pydantic AI coverage for ${{ github.sha }}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-html
path: htmlcov
include-hidden-files: true
# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
check:
if: always()
needs:
- lint
- mypy
- docs
- test
- test-lowest-versions
- test-examples
- coverage
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
with:
jobs: ${{ toJSON(needs) }}
# Note: this should match the `deploy-docs-manual` job in manually-deploy-docs.yml
deploy-docs:
needs: [check]
if: success() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Generate app token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
app-id: ${{ vars.DOCS_APP_ID }}
private-key: ${{ secrets.DOCS_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: unified-docs
- name: Trigger unified-docs deployment
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh api repos/pydantic/unified-docs/dispatches \
--method POST \
-f event_type=docs-update \
-f "client_payload[source]=pydantic/pydantic-ai@${{ github.sha }}"
deploy-docs-preview:
needs: [check]
if: success() && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: deploy-docs-preview
permissions:
deployments: write
statuses: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- run: npm install
working-directory: docs-site
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-suffix: deploy-docs-preview
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: site
path: site
- uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
id: deploy
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: previews
workingDirectory: docs-site
command: >
deploy
--var GIT_COMMIT_SHA:${{ github.sha }}
--var GIT_BRANCH:main
- name: Set preview URL
run: uv run --no-project --with httpx .github/set_docs_main_preview_url.py
env:
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
REF: ${{ github.sha }}
# Build wheels in an isolated job with no publish credentials. If anything in
# `uv build` (build backend, transitive build deps, restored cache) is
# compromised, the OIDC token for PyPI is in a separate job that only runs
# `pypi-publish` against the already-built artifacts.
release-build:
name: build release artifacts
needs: [check]
if: success() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
outputs:
package-version: ${{ steps.inspect_package.outputs.version }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
- run: uv build --all-packages
- name: Inspect package version
id: inspect_package
run: |
uv tool install --with uv-dynamic-versioning hatchling
version=$(uvx hatchling version)
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Upload distribution artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-dist
path: dist/
release:
name: publish to PyPI
needs: [release-build]
if: success() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
environment:
name: release
url: https://pypi.org/project/pydantic-ai/${{ needs.release-build.outputs.package-version }}
permissions:
id-token: write
outputs:
package-version: ${{ needs.release-build.outputs.package-version }}
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
skip-existing: true
send-tweet:
name: Send tweet
needs: [release]
if: needs.release.result == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- name: Install dependencies
run: pip install tweepy==4.14.0
- name: Send tweet
shell: python
run: |
import os
import tweepy
client = tweepy.Client(
access_token=os.getenv("TWITTER_ACCESS_TOKEN"),
access_token_secret=os.getenv("TWITTER_ACCESS_TOKEN_SECRET"),
consumer_key=os.getenv("TWITTER_CONSUMER_KEY"),
consumer_secret=os.getenv("TWITTER_CONSUMER_SECRET"),
)
version = os.getenv("VERSION").strip('"')
tweet = os.getenv("TWEET").format(version=version)
client.create_tweet(text=tweet)
env:
VERSION: ${{ needs.release.outputs.package-version }}
TWEET: |
Pydantic AI version {version} is out! 🎉
https://github.com/pydantic/pydantic-ai/releases/tag/v{version}
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
+106
View File
@@ -0,0 +1,106 @@
name: Docs Preview
# Label-triggered docs preview. Adding `trigger:docs` to a PR dispatches a
# preview build to pydantic/unified-docs; that repo builds the unified docs
# against this PR's head SHA, deploys to a shared Cloudflare Worker, and
# updates a comment on this PR with the preview URL.
#
# This workflow is purely a dispatcher — the build, deploy, and final
# URL-comment all happen in the unified-docs repo. The `trigger:docs` label
# is removed at the end so re-adding it re-runs the preview.
on:
# zizmor: ignore[dangerous-triggers] -- pull_request_target is required so fork PRs can
# access the DOCS_APP credentials. The dispatch is gated on a maintainer adding the
# `trigger:docs` label, and this workflow does not check out or execute PR code.
pull_request_target:
types: [labeled]
permissions: {}
concurrency:
group: docs-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
dispatch:
if: github.event.label.name == 'trigger:docs'
runs-on: ubuntu-latest
permissions:
# Needed to post the "queued" comment and to remove the trigger:docs label.
pull-requests: write
steps:
- name: Generate app token (for dispatching to unified-docs)
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.DOCS_APP_ID }}
private-key: ${{ secrets.DOCS_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: unified-docs
- name: Dispatch preview build
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
run: |
gh api repos/pydantic/unified-docs/dispatches \
--method POST \
-f event_type=docs-preview \
-f "client_payload[library]=ai" \
-f "client_payload[source_repo]=${REPO}" \
-f "client_payload[source_pr]=${PR_NUMBER}" \
-f "client_payload[source_sha]=${HEAD_SHA}"
- name: Acknowledge on PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
run: |
sha7=$(echo "$HEAD_SHA" | cut -c1-7)
body=$(cat <<EOF
## Docs Preview — queued
The preview build for commit \`${sha7}\` has been dispatched to [pydantic/unified-docs](https://github.com/pydantic/unified-docs/actions/workflows/docs-preview.yml). This comment will be updated with the preview URL once the build completes.
EOF
)
# Update an existing "Docs Preview" comment if present (covers re-runs); otherwise post a new one.
existing=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | startswith("## Docs Preview")))][0].url // empty')
if [ -n "$existing" ]; then
gh api -X PATCH "$existing" -f body="$body"
else
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" -f body="$body"
fi
- name: Comment failure on PR
if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
body=$(cat <<EOF
## Docs Preview — dispatch failed
The dispatch to pydantic/unified-docs failed. See [the workflow run](${RUN_URL}) for details.
<sub>Re-add the \`trigger:docs\` label to retry.</sub>
EOF
)
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" -f body="$body"
- name: Remove trigger:docs label
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: gh api -X DELETE "repos/${REPO}/issues/${PR_NUMBER}/labels/trigger:docs" || true
@@ -0,0 +1,66 @@
name: Gateway Model Health
on:
schedule:
# Run weekly on Monday at 10:00 UTC.
- cron: '0 10 * * 1'
workflow_dispatch: {}
env:
COLUMNS: 150
UV_PYTHON: 3.12
UV_FROZEN: "1"
permissions:
contents: read
jobs:
gateway-model-health:
environment:
name: gateway-model-health
runs-on: ubuntu-latest
timeout-minutes: 30
env:
CI: true
PYDANTIC_AI_GATEWAY_API_KEY: ${{ secrets.PYDANTIC_AI_GATEWAY_API_KEY }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
python-version: "3.12"
enable-cache: true
cache-suffix: gateway-model-health
- name: Install dependencies
run: uv sync --all-extras
- name: Run gateway model catalog checks
run: uv run pytest tests/providers/test_gateway_catalog.py -q --run-gateway-live
- name: Notify Slack on failure
if: failure() && !cancelled()
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
errors: true
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: ":x: Gateway model health failed in ${{ github.repository }} on ${{ github.ref_name }}."
blocks:
- type: section
text:
type: mrkdwn
text: ":x: *Gateway model health failed*"
- type: section
fields:
- type: mrkdwn
text: "*Repository*\n<${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>"
- type: mrkdwn
text: "*Trigger*\n${{ github.event_name }}"
- type: mrkdwn
text: "*Ref*\n`${{ github.ref_name }}`"
- type: mrkdwn
text: "*Run*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>"
+39
View File
@@ -0,0 +1,39 @@
name: Harness Compat
# Verifies that pydantic-ai changes don't break the pydantic-ai-harness lint /
# typecheck / test suite. Calls the harness's `compat-test.yml` reusable
# workflow with the change's HEAD SHA + repo (so fork PRs are tested against
# the code in the fork, not main).
#
# Triggers:
# - PRs touching `pydantic_ai_slim/**`: required to merge.
# - Pushes to `main`: confirms the merged commit still works.
# - Push of `v*` tags: gates a release on harness compatibility.
#
# Fork PRs run with `contents: read` only and no secrets passed to the called
# workflow — same exposure as the regular test jobs in `ci.yml`. If the called
# workflow ever grows steps that need elevated permissions or secrets, add a
# fork gate (e.g. require a `safe-for-ci` label) before merging that change.
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'pydantic_ai_slim/**'
- '.github/workflows/harness-compat.yml'
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
jobs:
harness-compat:
name: harness compat
# Same-org reusable workflow under shared maintenance; SHA pinning would force a coordination bump
# on every harness change without security benefit. Ignore configured in `.github/zizmor.yml`.
uses: pydantic/pydantic-ai-harness/.github/workflows/compat-test.yml@main
with:
pydantic-ai-ref: ${{ github.event.pull_request.head.sha || github.sha }}
pydantic-ai-repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
+34
View File
@@ -0,0 +1,34 @@
name: Link check
on:
schedule:
- cron: '0 0 * * 1' # weekly on Monday
workflow_dispatch:
permissions: {}
jobs:
link-check:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check links
id: lychee
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0
with:
args: --no-progress --config .github/workflows/lychee.toml './docs/**/*.md' './*.md'
fail: false
- name: Open issue on broken links
if: steps.lychee.outputs.exit_code != 0
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
with:
title: Broken links found in docs
content-filepath: ./lychee/out.md
labels: docs
+33
View File
@@ -0,0 +1,33 @@
# Configuration for the scheduled docs link check (see link-check.yml).
# Tuned to avoid false positives from sites that block bots or rate-limit, so the
# issue it opens only lists genuinely broken links.
# Some sites return 403/429 to non-browser clients even though the link is fine.
accept = ["200..=299", "403", "429"]
# Browser-like UA reduces bot-blocking 403s.
user_agent = "Mozilla/5.0 (compatible; lychee link checker)"
max_redirects = 10
max_retries = 3
retry_wait_time = 2
timeout = 30
# Don't check localhost example URLs or form-submission endpoints.
exclude_all_private = true
exclude_loopback = true
# Relative doc links are resolved by mkdocs, not as filesystem paths; skip the
# `file://` links lychee derives from them.
scheme = ["https", "http"]
# Login-gated or bot-blocking hosts that return non-2xx to automated checks even
# though the links are valid in a browser; not worth failing the run over.
exclude = [
"platform\\.openai\\.com",
"openai\\.com",
"console\\.mistral\\.ai",
"dash\\.voyageai\\.com",
"spec\\.modelcontextprotocol\\.io",
"customerioforms\\.com",
]
@@ -0,0 +1,30 @@
name: Manual Docs Deploy
on:
workflow_dispatch:
permissions:
contents: read
jobs:
# Note: this should match the `deploy-docs` job in ci.yml
deploy-docs-manual:
runs-on: ubuntu-latest
steps:
- name: Generate app token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
app-id: ${{ vars.DOCS_APP_ID }}
private-key: ${{ secrets.DOCS_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: unified-docs
- name: Trigger unified-docs deployment
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh api repos/pydantic/unified-docs/dispatches \
--method POST \
-f event_type=docs-update \
-f "client_payload[source]=pydantic/pydantic-ai (manual)"
+421
View File
@@ -0,0 +1,421 @@
name: PR Guard
on:
# zizmor: ignore[dangerous-triggers] -- pull_request_target is needed to close duplicate/unlinked PRs and manage issues; no fork code is checked out
pull_request_target:
types: [opened, reopened, ready_for_review, edited]
permissions: {}
concurrency:
group: pr-guard-${{ github.event.pull_request.number }}
jobs:
guard:
name: Guard
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Check linked issues and guard against duplicates
run: |
set -euo pipefail
PR_NUMBER="${GITHUB_EVENT_PULL_REQUEST_NUMBER}"
PR_AUTHOR="${GITHUB_EVENT_PULL_REQUEST_USER_LOGIN}"
AUTHOR_ASSOCIATION="${GITHUB_EVENT_PULL_REQUEST_AUTHOR_ASSOCIATION}"
echo "PR #${PR_NUMBER} by ${PR_AUTHOR} (${AUTHOR_ASSOCIATION})"
# `edited` also fires on closed and merged PRs (e.g. an author updating
# the description before reopening, as the close comment suggests, or a
# bot rewriting the description). Nothing to check, and `gh pr close`
# on an already-closed PR would fail the job.
if [ "${GITHUB_EVENT_PULL_REQUEST_STATE}" != "open" ]; then
echo "PR is ${GITHUB_EVENT_PULL_REQUEST_STATE}, skipping checks."
exit 0
fi
# Skip draft PRs — authors may create drafts to save progress
if [ "${GITHUB_EVENT_PULL_REQUEST_DRAFT}" = "true" ]; then
echo "PR is a draft, skipping checks."
exit 0
fi
# Only check PRs targeting the default branch. GitHub interprets closing
# keywords ("Fixes #123") *only* on default-branch PRs; on any other base
# branch they're ignored and closingIssuesReferences is always empty, so
# enforcing the issue-link policy there would close validly-linked PRs (e.g.
# a fix targeting a maintenance branch). Fail open if either value is empty.
if [ -z "${DEFAULT_BRANCH}" ] || [ -z "${GITHUB_EVENT_PULL_REQUEST_BASE_REF}" ] || [ "${GITHUB_EVENT_PULL_REQUEST_BASE_REF}" != "${DEFAULT_BRANCH}" ]; then
echo "PR base '${GITHUB_EVENT_PULL_REQUEST_BASE_REF}' is not the default branch '${DEFAULT_BRANCH}'; skipping checks."
exit 0
fi
# Maintainer bypass
if [[ "$AUTHOR_ASSOCIATION" == "MEMBER" || "$AUTHOR_ASSOCIATION" == "OWNER" || "$AUTHOR_ASSOCIATION" == "COLLABORATOR" ]]; then
echo "Author is a maintainer/collaborator, skipping checks."
exit 0
fi
# Author repo-role bypass. The webhook's `author_association` is unreliable — it
# can report CONTRIBUTOR for a genuine org member/collaborator (observed on #6359:
# `dsfaccini`, a public org member with `maintain` role, came through as
# CONTRIBUTOR, so the bypass above missed and the PR was wrongly closed). Confirm
# the author's actual role on THIS repo via the API — the same `role_name` signal
# the trusted-actor bypass below uses for the sender — and skip for triage-or-higher.
# Fails open toward *checking* (an unreadable/unknown role continues the checks), so
# an external contributor can never gain a bypass this way.
AUTHOR_ROLE=$(gh api "repos/${REPO}/collaborators/${PR_AUTHOR}/permission" --jq '.role_name' 2>/dev/null || echo "unknown")
case "$AUTHOR_ROLE" in
triage | write | maintain | admin)
echo "Author ${PR_AUTHOR} has ${AUTHOR_ROLE} access to this repo; skipping checks."
exit 0
;;
*)
echo "Author ${PR_AUTHOR} repo role: ${AUTHOR_ROLE}; continuing checks."
;;
esac
# Private-membership bypass. The `author_association` in the webhook only
# reflects *public* org membership, so a private member of the org that
# owns this repo is reported as CONTRIBUTOR and misses the bypass above.
# Confirm real membership via the API, which sees private members when the
# token has `read:org` — the default `github.token` cannot, so this uses
# `ORG_READ_TOKEN` (an existing org-scoped secret). Fails open: if that
# token is unset or lacks the scope the API call errors and the existing
# policy applies unchanged, so no unlinked PR is ever wrongly kept open.
if [ -n "${ORG_READ_TOKEN:-}" ] && GH_TOKEN="$ORG_READ_TOKEN" gh api "orgs/${REPO%/*}/members/${PR_AUTHOR}" >/dev/null 2>&1; then
echo "Author ${PR_AUTHOR} is an org member (confirmed via API, incl. private membership); skipping checks."
exit 0
fi
# Trusted-actor bypass. AUTHOR_ASSOCIATION above is the PR *author's*
# relationship to the repo, so without this the guard re-closes a
# contributor PR every time a maintainer acts on it: reopening it, or
# editing its title/body (`reopened` and `edited` both re-run this
# workflow, so a reopen-only bypass gets undone by the next edit). When
# the person who triggered this run isn't the author, confirm they have
# triage-or-higher access on *this* repo and, if so, treat their action
# as a deliberate decision to keep the PR open and skip the checks.
# - We read `role_name`, not the legacy `permission` (which collapses
# triage into read), so a triager — who can legitimately reopen —
# still counts.
# - We require `sender != author` and a base-repo role because GitHub
# also lets a collaborator on the contributor's *fork* reopen the PR;
# `sender != author` alone isn't proof of base-repo trust.
# - If the role can't be determined, err toward respecting the action:
# this is a courtesy gate (trivially satisfied by linking any open
# issue), not a security boundary, and a deliberate maintainer action
# shouldn't be undone by a transient API hiccup.
# The author acting on their own PR (e.g. the `opened` event, where the
# sender is always the author) still gets checked, so the issue-link
# policy can't be bypassed this way.
if [ "${GITHUB_EVENT_SENDER_LOGIN}" != "$PR_AUTHOR" ]; then
SENDER_ROLE=$(gh api "repos/${REPO}/collaborators/${GITHUB_EVENT_SENDER_LOGIN}/permission" --jq '.role_name' 2>/dev/null || echo "unknown")
case "$SENDER_ROLE" in
read | none)
echo "Event triggered by ${GITHUB_EVENT_SENDER_LOGIN} (role: ${SENDER_ROLE}, no write access); continuing checks."
;;
*)
echo "Event triggered by ${GITHUB_EVENT_SENDER_LOGIN} (role: ${SENDER_ROLE}); a trusted collaborator acted on this PR, skipping checks."
exit 0
;;
esac
fi
# pydanty is the autonomous agent that opens PRs from issues. Its PRs are
# never auto-closed for duplication; instead a pydanty PR supersedes a
# competing PR opened within WINDOW_SECONDS before it (see the loop below).
PYDANTY_LOGIN="pydanty[bot]"
WINDOW_SECONDS=600
IS_PYDANTY_PR="false"
if [ "$PR_AUTHOR" = "$PYDANTY_LOGIN" ]; then
IS_PYDANTY_PR="true"
fi
PR_CREATED_EPOCH=$(date -u -d "$GITHUB_EVENT_PULL_REQUEST_CREATED_AT" +%s)
# Helper: close a contributor PR that doesn't link to any existing issue.
# Only external contributors reach this point (maintainers/collaborators
# bypass above). Exemptions:
# - bot authors (pydanty, dependabot, etc.) — they never file issues first
# - docs-only changes — small doc fixes are welcome without an issue
close_for_missing_issue() {
case "$PR_AUTHOR" in
*"[bot]")
echo "Author ${PR_AUTHOR} is a bot; not closing for missing issue link."
return 0
;;
esac
# If the changed files can't be determined, err on the side of leaving
# the PR open: closing is destructive and the docs-only exemption
# can't be ruled out.
if ! CHANGED_FILES=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files?per_page=100" --paginate --jq '.[].filename') || [ -z "$CHANGED_FILES" ]; then
echo "Could not determine changed files; not closing for missing issue link."
return 0
fi
DOCS_ONLY="true"
while IFS= read -r CHANGED_FILE; do
case "$CHANGED_FILE" in
docs/*|*.md|mkdocs.yml) ;;
*) DOCS_ONLY="false"; break ;;
esac
done <<< "$CHANGED_FILES"
if [ "$DOCS_ONLY" = "true" ]; then
echo "PR only touches documentation; not closing for missing issue link."
return 0
fi
echo "Closing PR #${PR_NUMBER} — no linked issue."
COMMENT=$(printf '%s\n\n%s\n\n%s\n\n%s' \
"Thanks for the contribution! To make sure changes are discussed before code is written, we ask that every PR references an existing issue with a closing keyword (e.g. \`Fixes #1234\`) in its description, so this PR has been closed automatically." \
"If there's no issue for this yet, please open one first (e.g. a bug report with a reproducible example), wait for a maintainer to confirm it, then update this PR's description to reference it and reopen the PR." \
"Documentation-only fixes are exempt and don't need an issue." \
"Feel free to ping our team if you think this was closed in error.")
# Comment before closing so the PR can never end up closed without an
# explanation; if commenting fails, set -e stops us and the PR stays open.
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$COMMENT"
gh pr close "$PR_NUMBER" --repo "$REPO"
}
# Ask GitHub which issues this PR would close, rather than parsing the
# body ourselves: GitHub's own parser is the authority on closing-keyword
# syntax ("Fixes #123", "Fixes: #123", "Fixes owner/repo#123", full issue
# URLs) and only counts references that resolve to real issues. Closed
# issues are included (with state CLOSED), so the checks below still see
# them. References to issues in other repos don't count: the policy
# requires an issue in this repo. If the query fails, set -e fails the
# job and the PR is left open.
fetch_linked_issues() {
gh api graphql \
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$PR_NUMBER" \
-f query='
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
closingIssuesReferences(first: 100) {
nodes { number repository { nameWithOwner } }
}
}
}
}' \
--jq ".data.repository.pullRequest.closingIssuesReferences.nodes[] | select(.repository.nameWithOwner == \"${REPO}\") | .number"
}
ISSUE_NUMBERS=$(fetch_linked_issues)
if [ -z "$ISSUE_NUMBERS" ]; then
# GitHub resolves closing references asynchronously, so a query right
# after a PR is opened or edited can transiently come back empty.
# Closing is destructive — wait and confirm before acting on it.
echo "No linked issues found; re-checking in 30s in case GitHub hasn't resolved references yet."
sleep 30
ISSUE_NUMBERS=$(fetch_linked_issues)
fi
if [ -z "$ISSUE_NUMBERS" ]; then
echo "No linked issues found in PR body."
close_for_missing_issue
exit 0
fi
echo "Found linked issues: $ISSUE_NUMBERS"
# Helper: attempt to assign a user to an issue.
# GitHub silently ignores assignees who lack push access (returns 201 but doesn't add them).
# We check assignability first via GET /repos/{owner}/{repo}/assignees/{user} (204 = yes, 404 = no).
try_assign() {
local issue_num="$1"
local user="$2"
if gh api "repos/${REPO}/assignees/${user}" > /dev/null 2>&1; then
gh api "repos/${REPO}/issues/${issue_num}/assignees" --method POST -f "assignees[]=${user}" > /dev/null
echo "Assigned ${user} to issue #${issue_num}."
return 0
else
echo "Cannot assign ${user} to issue #${issue_num} (user is not assignable to this repo)."
return 1
fi
}
FOUND_OPEN_ISSUE="false"
FOUND_CLOSED_ISSUE="false"
for ISSUE_NUM in $ISSUE_NUMBERS; do
echo ""
echo "--- Checking issue #${ISSUE_NUM} ---"
# Issues come from closingIssuesReferences, so they are known to exist;
# a fetch failure here is transient. Let set -e fail the job (leaving
# the PR open) rather than treating the reference as unresolved, which
# could get a validly-linked PR closed.
ISSUE_JSON=$(gh api "repos/${REPO}/issues/${ISSUE_NUM}")
# Skip if this is actually a pull request
IS_PR=$(echo "$ISSUE_JSON" | jq 'has("pull_request")')
if [ "$IS_PR" = "true" ]; then
echo "#${ISSUE_NUM} is a pull request, not an issue. Skipping."
continue
fi
# Skip closed issues — duplicate check only applies to open issues
ISSUE_STATE=$(echo "$ISSUE_JSON" | jq -r '.state')
if [ "$ISSUE_STATE" != "open" ]; then
echo "Issue #${ISSUE_NUM} is ${ISSUE_STATE}, skipping."
FOUND_CLOSED_ISSUE="true"
continue
fi
FOUND_OPEN_ISSUE="true"
ISSUE_AUTHOR=$(echo "$ISSUE_JSON" | jq -r '.user.login')
# Check for duplicate/blocking PRs first — this must run regardless of assignment outcome.
# As with the current PR's linked issues, ask GitHub which open PRs
# would close this issue instead of regex-matching PR bodies, so all
# closing syntaxes (and manually linked PRs) are recognized. Bot
# logins get their "[bot]" suffix restored to match the REST-style
# logins used elsewhere (PYDANTY_LOGIN, ISSUE_AUTHOR).
# Only PRs created before this one can block it: two PRs racing each
# other's guard runs would otherwise both see the other as open and
# both get closed, and a newer PR (e.g. pydanty's, deliberately left
# alongside an older human PR) must not close the older one when an
# edit re-triggers the guard. The pydanty supersede logic below is
# unaffected: it only ever targets PRs opened before pydanty's.
echo "Checking for existing PRs targeting issue #${ISSUE_NUM}..."
BLOCKING_PRS=""
FOUND_STALE_PR="false"
MATCHING_PRS=$(gh api graphql \
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="$ISSUE_NUM" \
-f query='
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
issue(number: $number) {
closedByPullRequestsReferences(first: 100) {
nodes {
number
state
createdAt
repository { nameWithOwner }
author { __typename login }
labels(first: 100) { nodes { name } }
}
}
}
}
}' \
--jq ".data.repository.issue.closedByPullRequestsReferences.nodes[]
| select(.state == \"OPEN\" and .repository.nameWithOwner == \"${REPO}\" and .number != ${PR_NUMBER} and .createdAt < \"${GITHUB_EVENT_PULL_REQUEST_CREATED_AT}\")
| {number, created_at: .createdAt, user: {login: (if .author.__typename == \"Bot\" then .author.login + \"[bot]\" else .author.login end)}, labels: [.labels.nodes[]]}")
while IFS= read -r PR_JSON; do
[ -z "$PR_JSON" ] && continue
EXISTING_PR_NUM=$(echo "$PR_JSON" | jq -r '.number')
EXISTING_PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.user.login')
echo "Found PR #${EXISTING_PR_NUM} by ${EXISTING_PR_AUTHOR} that references issue #${ISSUE_NUM}."
# Check if the existing PR has the Stale label
HAS_STALE=$(echo "$PR_JSON" | jq '[.labels[].name] | any(. == "Stale")')
if [ "$HAS_STALE" = "true" ]; then
echo "PR #${EXISTING_PR_NUM} is stale. Allowing new PR to supersede."
FOUND_STALE_PR="true"
continue
fi
# pydanty's own PR is never closed here. Instead it supersedes a
# competing PR that opened within WINDOW_SECONDS before it, unless
# that PR belongs to the issue author (who keeps priority).
if [ "$IS_PYDANTY_PR" = "true" ]; then
if [ "$EXISTING_PR_AUTHOR" = "$PYDANTY_LOGIN" ]; then
echo "PR #${EXISTING_PR_NUM} is also pydanty's; leaving it."
continue
fi
if [ "$EXISTING_PR_AUTHOR" = "$ISSUE_AUTHOR" ]; then
echo "PR #${EXISTING_PR_NUM} is by the issue author (${ISSUE_AUTHOR}); leaving both open for a maintainer."
continue
fi
EXISTING_EPOCH=$(date -u -d "$(echo "$PR_JSON" | jq -r '.created_at')" +%s)
DELTA=$(( PR_CREATED_EPOCH - EXISTING_EPOCH ))
if [ "$DELTA" -ge 0 ] && [ "$DELTA" -le "$WINDOW_SECONDS" ]; then
echo "pydanty PR #${PR_NUMBER} opened ${DELTA}s after PR #${EXISTING_PR_NUM} (<= ${WINDOW_SECONDS}s); superseding it."
SUPERSEDE_COMMENT=$(printf '%s\n\n%s' \
"An automated pull request from @${PYDANTY_LOGIN} addressing issue #${ISSUE_NUM} opened within ~10 minutes of this one, so it is taking precedence and this PR has been closed to avoid duplicate effort." \
"Thanks for contributing — if you'd like to keep working on this, please comment on [issue #${ISSUE_NUM}](https://github.com/${REPO}/issues/${ISSUE_NUM}) and a maintainer can help coordinate.")
gh pr comment "$EXISTING_PR_NUM" --repo "$REPO" --body "$SUPERSEDE_COMMENT"
gh pr close "$EXISTING_PR_NUM" --repo "$REPO"
else
echo "PR #${EXISTING_PR_NUM} opened more than ${WINDOW_SECONDS}s before pydanty's; leaving both open for a maintainer."
fi
continue
fi
# Collect all non-stale blocking PRs
if [ -z "$BLOCKING_PRS" ]; then
BLOCKING_PRS="#${EXISTING_PR_NUM}"
else
BLOCKING_PRS="${BLOCKING_PRS}, #${EXISTING_PR_NUM}"
fi
done <<< "$MATCHING_PRS"
if [ "$IS_PYDANTY_PR" != "true" ] && [ -n "$BLOCKING_PRS" ]; then
echo "Closing PR #${PR_NUMBER} — issue #${ISSUE_NUM} already has active PRs: ${BLOCKING_PRS}"
COMMENT=$(printf '%s\n\n%s\n\n%s' \
"Thanks for your interest in this issue! However, there are already open PRs addressing issue #${ISSUE_NUM}: ${BLOCKING_PRS}." \
"To avoid duplicate efforts, this PR has been closed. If you'd like to contribute, you can review the existing PRs or share your thoughts on [issue #${ISSUE_NUM}](https://github.com/${REPO}/issues/${ISSUE_NUM})." \
"If you believe the existing PRs are inactive, please comment on the issue and a maintainer can reassess.")
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$COMMENT"
gh pr close "$PR_NUMBER" --repo "$REPO"
exit 0
fi
if [ "$FOUND_STALE_PR" = "true" ]; then
echo "All existing PRs for issue #${ISSUE_NUM} are stale. Allowing new PR."
fi
# Now handle assignment (best-effort, does not gate duplicate check above)
ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '[.assignees[].login] | join(",")')
echo "Current assignees: ${ASSIGNEES:-none}"
if [ -z "$ASSIGNEES" ]; then
# No assignee — attempt to assign PR author (may fail if user lacks push access)
try_assign "$ISSUE_NUM" "$PR_AUTHOR" || true
elif echo ",$ASSIGNEES," | grep -qF ",${PR_AUTHOR},"; then
echo "Issue #${ISSUE_NUM} is already assigned to ${PR_AUTHOR}."
else
echo "Issue #${ISSUE_NUM} is assigned to someone else. Leaving assignment as-is for maintainers to review."
fi
done
# If none of the referenced numbers resolved to an actual issue (open or
# closed), the PR is effectively unlinked — same policy as no keywords at all.
if [ "$FOUND_OPEN_ISSUE" = "false" ] && [ "$FOUND_CLOSED_ISSUE" = "false" ]; then
echo "No referenced numbers resolved to actual issues."
close_for_missing_issue
exit 0
fi
# If we found closed issues but no open ones, close the PR (pydanty PRs are exempt).
if [ "$IS_PYDANTY_PR" != "true" ] && [ "$FOUND_CLOSED_ISSUE" = "true" ] && [ "$FOUND_OPEN_ISSUE" = "false" ]; then
echo "All referenced issues are closed. Closing PR."
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "All issues referenced by this PR are already closed. If you believe an issue should be reopened, please comment on it first."
gh pr close "$PR_NUMBER" --repo "$REPO"
fi
env:
GH_TOKEN: ${{ github.token }}
# Read-only org-scoped token, used solely to confirm private org
# membership (see the private-membership bypass). Falls back to empty
# when unset, which the bypass handles by failing open.
ORG_READ_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
REPO: ${{ github.repository }}
GITHUB_EVENT_SENDER_LOGIN: ${{ github.event.sender.login }}
GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_EVENT_PULL_REQUEST_USER_LOGIN: ${{ github.event.pull_request.user.login }}
GITHUB_EVENT_PULL_REQUEST_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
GITHUB_EVENT_PULL_REQUEST_CREATED_AT: ${{ github.event.pull_request.created_at }}
GITHUB_EVENT_PULL_REQUEST_DRAFT: ${{ github.event.pull_request.draft }}
GITHUB_EVENT_PULL_REQUEST_STATE: ${{ github.event.pull_request.state }}
GITHUB_EVENT_PULL_REQUEST_BASE_REF: ${{ github.event.pull_request.base.ref }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
---
emoji: "🐛"
name: "Pydantic AI Bug Hunter"
description: "Find a reproducible, user-impacting bug in pydantic-ai and file a report issue. Runs on the Pydantic AI gh-aw shim; the task prompt is iterable from a Logfire managed variable."
on: weekly on thursday
permissions:
contents: read
issues: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-bug-hunter
cancel-in-progress: true
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
footer: false
activation-comments: false
noop:
create-issue:
max: 1
title-prefix: "[bug-hunter] "
close-older-key: "[bug-hunter]"
close-older-issues: false
expires: 7d
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/adversarial-review.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-bug-hunter.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_bug_hunter_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-bug-hunter.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
---
emoji: "📚"
name: "Pydantic AI Docs Drift"
description: "Detect negative docs drift where existing documentation no longer matches the codebase, and file an issue. Runs on the Pydantic AI gh-aw shim; the task prompt is iterable from a Logfire managed variable."
on: weekly on monday
permissions:
contents: read
issues: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-docs-drift
cancel-in-progress: true
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
footer: false
activation-comments: false
noop:
create-issue:
max: 1
title-prefix: "[docs-drift] "
labels: [docs-drift]
close-older-key: "[docs-drift]"
close-older-issues: false
expires: 7d
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-docs-drift.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_docs_drift_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-docs-drift.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
+137
View File
@@ -0,0 +1,137 @@
---
emoji: "🔎"
name: "Pydantic AI PR Review"
description: "AI-driven PR review on the Pydantic AI gh-aw shim: inline comments + a single review verdict. Prompt iterable from a Logfire managed variable; read-only via gh-aw safe-outputs."
on:
pull_request:
types: [opened, synchronize, ready_for_review]
workflow_dispatch:
# Fork-PR safety: only trigger when the actor has admin/maintainer/write
# access. Without this, any established external contributor's PR would
# consume the configured Anthropic key and a model run.
roles: [admin, maintainer, write]
# Skip if PR has the `auto-review` label (opts into the legacy bots.yml reviewer).
if: ${{ !contains(github.event.pull_request.labels.*.name, 'auto-review') }}
permissions:
contents: read
# safe-outputs perform the actual writes in a separate conclusion job; the
# agent job stays read-only (gh-aw strict mode requires this).
pull-requests: read
issues: read
concurrency:
# One review per PR; newer pushes supersede in-flight reviews.
group: ${{ github.workflow }}-pr-review-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
tools:
github:
mode: gh-proxy
# PR-scoped surface: read the PR, related issues, repo, and search.
toolsets: [pull_requests, repos, search, issues]
safe-outputs:
footer: false
activation-comments: false
noop:
create-pull-request-review-comment:
max: 30
submit-pull-request-review:
supersede-older-reviews: true
max: 1
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/review-context.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
pre-steps:
# Setting engine.command makes gh-aw skip ALL engine installation steps,
# which also drops the bundled AWF firewall binary install. Re-run gh-aw's
# own installer (the same call it makes for non-custom-command jobs).
- name: Install AWF firewall binary (skipped by custom engine.command)
run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46
pre-agent-steps:
# Stage the committed launcher script at gh-aw's exec-able
# /tmp/gh-aw/bin/ path. Runs in pre-agent-steps (not pre-steps) because
# gh-aw's repository checkout happens between pre-steps and
# pre-agent-steps, and this step reads from .github/scripts/ in the
# workspace.
- name: Stage Pydantic AI gh-aw shim launcher
run: |
mkdir -p /tmp/gh-aw/bin
install -m 755 .github/scripts/pydantic-ai-runner-launch.sh /tmp/gh-aw/bin/pydantic-ai-runner-launch
# Warm the harness's uv script environment on the OPEN network so the
# firewalled agent reuses a warm cache (non-fatal on failure).
- name: Pre-warm Pydantic AI gh-aw shim uv environment
run: bash .github/scripts/prewarm-pydantic-ai-runner.sh
# Pre-fetch PR context into `/tmp/gh-aw/.review-context/`: pr-details, PR
# comments, review threads (with annotated diff hunks + resolved/outdated
# state), annotated per-file diffs, related issues, AGENTS.md excerpts for
# changed dirs, file orderings for sub-agent fan-out, and a PR-size summary.
# The agent reads these files instead of calling the GitHub API at run time.
# Non-fatal: missing context just reduces signal.
#
# The script lives at scripts/ (NOT .github/scripts/) because gh-aw's
# "Save/Restore agent config folders from base branch" step snapshots and
# restores `.github/` (and other managed agent-config folders) from the
# BASE branch — making any new file added under those folders unreliable
# for steps that run after the restore. `scripts/` is outside that set,
# matching where the legacy reviewer's gather-review-context.sh already
# lives. The script is a fork of scripts/gather-review-context.sh — see
# the TODO at the top of the fork.
- name: Gather PR review context
if: ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -uo pipefail
# Diagnostic: log the workspace state of both scripts/ and .github/scripts/
# so we can verify the gh-aw restore-from-base step left them untouched.
echo "::group::workspace inventory at gather time"
ls -la scripts/ 2>&1 | head -50 || true
echo "---"
ls -la .github/scripts/ 2>&1 | head -50 || true
echo "::endgroup::"
script=scripts/gather-pydantic-ai-review-context.sh
if [ -x "$script" ]; then
"$script" "$PR_NUMBER" "$REPO" \
|| echo "::warning::${script} failed; reviewer will run with less context"
else
echo "::warning::${script} not present; reviewer will run with less context"
fi
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-pr-review.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_pr_review_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-pr-review.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,68 @@
---
emoji: "🔌"
name: "Pydantic AI Provider Mapping Sweep"
description: "Audit one model provider's request/response mapping against its SDK and file a reproducible bug. Rotates providers; runs on the Pydantic AI gh-aw shim; the prompt is iterable from a Logfire managed variable."
on: weekly on monday
permissions:
contents: read
issues: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-provider-mapping-sweep
cancel-in-progress: true
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
footer: false
activation-comments: false
noop:
create-issue:
max: 1
title-prefix: "[provider-mapping-sweep] "
labels: [provider-mapping-sweep]
close-older-key: "[provider-mapping-sweep]"
close-older-issues: false
expires: 7d
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/adversarial-review.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-provider-mapping-sweep.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_provider_mapping_sweep_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-provider-mapping-sweep.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,68 @@
---
emoji: "🧭"
name: "Pydantic AI Provider Parity Explore"
description: "Explore one cross-cutting capability's support across all providers and file an issue for concrete parity gaps. Runs on the Pydantic AI gh-aw shim; the prompt is iterable from a Logfire managed variable."
on: weekly on tuesday
permissions:
contents: read
issues: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-provider-parity-explore
cancel-in-progress: true
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
footer: false
activation-comments: false
noop:
create-issue:
max: 1
title-prefix: "[provider-parity-explore] "
labels: [provider-parity-explore]
close-older-key: "[provider-parity-explore]"
close-older-issues: false
expires: 7d
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/adversarial-review.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-provider-parity-explore.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_provider_parity_explore_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-provider-parity-explore.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,68 @@
---
emoji: "⏪"
name: "Pydantic AI Regression Detector"
description: "Detect behavioral regressions between the two most recent releases and file a reproducible report. Runs on the Pydantic AI gh-aw shim; the prompt is iterable from a Logfire managed variable."
on: weekly on wednesday
permissions:
contents: read
issues: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-regression-detector
cancel-in-progress: true
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
footer: false
activation-comments: false
noop:
create-issue:
max: 1
title-prefix: "[regression-detector] "
labels: [regression]
close-older-key: "[regression-detector]"
close-older-issues: false
expires: 7d
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/adversarial-review.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-regression-detector.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_regression_detector_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-regression-detector.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,72 @@
---
emoji: "♻️"
name: "Pydantic AI Round-Trip Sweep"
description: "Find serialize/deserialize state-loss bugs across a message round-trip boundary and file a reproducible report. Runs on the Pydantic AI gh-aw shim; the prompt is iterable from a Logfire managed variable."
on: daily
permissions:
contents: read
issues: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-roundtrip-sweep
cancel-in-progress: true
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
footer: false
activation-comments: false
# Engine/model failures are tracked as ERROR spans in Logfire (service_name
# `gh-aw.pydantic-ai-roundtrip-sweep`) via the otel-logfire import + the shim's
# `instrument_pydantic_ai`, so we don't also file an auto-generated failure issue.
report-failure-as-issue: false
noop:
create-issue:
max: 1
title-prefix: "[roundtrip-sweep] "
labels: [roundtrip-sweep]
close-older-key: "[roundtrip-sweep]"
close-older-issues: false
expires: 7d
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/adversarial-review.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-roundtrip-sweep.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_roundtrip_sweep_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-roundtrip-sweep.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,145 @@
---
emoji: "🔍"
name: "Pydantic AI Stale Issues Finder"
description: "Find open issues that are likely already resolved, obsolete, or tied to deprecated features, and file a report issue. Runs on the Pydantic AI gh-aw shim; the task prompt is iterable from a Logfire managed variable."
# Weekly on Monday: gh-aw scatters the run and auto-adds workflow_dispatch.
# Adjust to 'daily' or another weekly schedule to change frequency.
on: weekly on monday
permissions:
contents: read
issues: read
pull-requests: read
# Full git history: the agent needs `git log` to detect removed/renamed APIs
# referenced in open issues. fetch-depth: 0 gives the full commit history.
checkout:
fetch-depth: 0
concurrency:
group: ${{ github.workflow }}-stale-issues-finder
cancel-in-progress: true
network:
allowed:
- defaults
# Python/PyPI ecosystem — the harness installs its deps via `uv` at agent
# time; allow them through the AWF firewall.
- python
# ANTHROPIC_BASE_URL is a compile-time literal (below) so gh-aw already
# auto-allowlists the host; this explicit entry is a harmless safety net.
- api.minimax.io
# We register as the built-in `claude` engine and only override `command`, so
# gh-aw runs its full Claude proxy + credential-injection machinery for us.
# ANTHROPIC_BASE_URL MUST be a compile-time literal (not a ${{ vars.* }}
# expression): gh-aw derives the api-proxy target host AND the
# `--anthropic-api-base-path` from its parsed URL path at compile time. With a
# vars expression the path can't be parsed, so the proxy drops the `/anthropic`
# prefix and the upstream returns 404. Only ANTHROPIC_API_KEY stays a secret
# (injected by the AWF api-proxy, excluded from the agent container). MiniMax
# exposes an Anthropic-compatible API at https://api.minimax.io/anthropic.
runtimes:
uv: {}
engine:
id: claude
# Pulled from the repo's `vars.GH_AW_MODEL` (set out-of-band).
# gh-aw compiles this into the engine command's `--model <name>` argv,
# which the harness reads via `args.model`.
model: ${{ vars.GH_AW_MODEL }}
# The checked-out workspace is mounted no-exec in the AWF sandbox, so a
# pre-step stages a launcher in gh-aw's exec-able /tmp/gh-aw/bin that runs
# `uv run --script` against the workspace harness.
command: /tmp/gh-aw/bin/pydantic-ai-runner-launch
env:
ANTHROPIC_BASE_URL: https://api.minimax.io/anthropic
ANTHROPIC_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
# Hide gh-aw's "Generated by …" footer on every safe-output;
# hidden gh-aw-workflow-id / gh-aw-tracker-id markers still get emitted
# for search-ability.
footer: false
activation-comments: false
noop:
create-issue:
max: 1
title-prefix: "[stale-finder] "
labels: [stale-issues]
close-older-key: "[stale-finder]"
close-older-issues: false
expires: 7d
# Note: elastic uses 2d with twice-weekly schedule. Adjust 'expires'
# and the schedule together if you change run frequency.
timeout-minutes: 60
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
pre-steps:
# Setting engine.command makes gh-aw skip ALL engine installation steps,
# which also drops the bundled AWF firewall binary install. Re-run gh-aw's
# own installer (the same call it makes for non-custom-command jobs).
- name: Install AWF firewall binary (skipped by custom engine.command)
run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46
pre-agent-steps:
# Stage the committed launcher script at gh-aw's exec-able
# /tmp/gh-aw/bin/ path. Runs in pre-agent-steps (not pre-steps) because
# gh-aw's repository checkout happens between pre-steps and
# pre-agent-steps, and this step reads from .github/scripts/ in the
# workspace.
- name: Stage Pydantic AI gh-aw shim launcher
run: |
mkdir -p /tmp/gh-aw/bin
install -m 755 .github/scripts/pydantic-ai-runner-launch.sh /tmp/gh-aw/bin/pydantic-ai-runner-launch
# Install ripgrep and expose uv+rg inside the AWF chroot.
# AWF auto-merges /opt/hostedtoolcache/**/bin into the container PATH
# and also reads $GITHUB_PATH entries added before the engine step.
- name: Install tools for AWF sandbox (ripgrep)
run: bash .github/scripts/install-sandbox-tools.sh
# Warm the harness's uv script environment on the OPEN network so the
# firewalled agent reuses a warm cache (non-fatal on failure).
- name: Pre-warm Pydantic AI gh-aw shim uv environment
run: bash .github/scripts/prewarm-pydantic-ai-runner.sh
# Fetch all open issues before the AWF firewall blocks gh CLI access.
# The script writes one JSON file per issue and pre-groups issues into
# batch folders for subagent fan-out.
- name: Prescan open issues and build batch folders
env:
GH_TOKEN: ${{ github.token }}
# One file per issue under /tmp/gh-aw/agent/issues/all.
# Batches under /tmp/gh-aw/agent/issues/batches/batch-XXX.
BATCH_SIZE: 25
ISSUE_LIMIT: 1000
run: |
bash .github/scripts/prefetch-open-issues.sh
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-stale-issues-finder.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_stale_issues_finder_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-stale-issues-finder.md
logfire-read-key: ${{ secrets.LOGFIRE_PROMPT_TOKEN }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-eu.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,68 @@
---
emoji: "🌊"
name: "Pydantic AI Streaming Resilience Sweep"
description: "Audit the streaming state machine for ordering/lifecycle bugs and file a reproducible report. Runs on the Pydantic AI gh-aw shim; the prompt is iterable from a Logfire managed variable."
on: weekly on saturday
permissions:
contents: read
issues: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-streaming-resilience-sweep
cancel-in-progress: true
tools:
github:
mode: gh-proxy
toolsets: [default]
safe-outputs:
footer: false
activation-comments: false
noop:
create-issue:
max: 1
title-prefix: "[streaming-resilience-sweep] "
labels: [streaming]
close-older-key: "[streaming-resilience-sweep]"
close-older-issues: false
expires: 7d
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/adversarial-review.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
jobs:
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-streaming-resilience-sweep.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_streaming_resilience_sweep_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-streaming-resilience-sweep.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,154 @@
---
emoji: "🛡️"
name: "Pydantic AI UI Security Review"
description: "Security review of UI-adapter PRs (Vercel AI + AG-UI): audits the client/server trust boundary for outbound leakage and inbound abuse. Inline comments + a non-voting COMMENT-type review summary (pydantic-ai-pr-review owns the merge-gate verdict until gh-aw check-runs land). Prompt iterable from a Logfire managed variable; read-only via gh-aw safe-outputs."
on:
# Runs on EVERY PR (no `paths:` filter) so the review's check is always
# reported on the head commit. The UI-path selection moved into the `detect`
# job below: non-UI PRs skip the agent via `if:` (a job skipped by `if:`
# reports as success, so it never blocks merge), while UI PRs gate the agent
# behind the `ui-security-review` Environment (required reviewers) — a
# maintainer clicks "Approve" to start the AI run, and the pending approval
# blocks merge until the review has run. `synchronize` is kept so a new
# commit re-reports the check on the new head and re-pends the approval.
pull_request:
types: [opened, synchronize, ready_for_review]
workflow_dispatch:
# Fork-PR safety: only trigger when the actor has admin/maintainer/write
# access. Without this, any established external contributor's PR would
# consume the configured Anthropic key and a model run.
roles: [admin, maintainer, write]
# Pause for a maintainer's approval of the `ui-security-review` Environment
# before the agent runs. This gates the `activation` job, but because
# `activation` is itself gated by the `if:` below, the approval is only
# requested on UI-touching PRs (a skipped job never requests Environment
# approval). The pending approval keeps the agent's required check pending,
# blocking merge until a maintainer clicks Approve to start the review.
manual-approval: ui-security-review
# Only run the review (and request approval) when the PR touches the UI
# security surface. Non-UI PRs skip the activation chain, so the agent reports
# "skipped" (= success for required checks) and never blocks merge.
if: ${{ needs.detect.outputs.touched == 'true' }}
permissions:
contents: read
# safe-outputs perform the actual writes in a separate conclusion job; the
# agent job stays read-only (gh-aw strict mode requires this).
pull-requests: read
issues: read
concurrency:
# One security review per PR; newer pushes supersede in-flight reviews.
group: ${{ github.workflow }}-ui-security-review-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
tools:
github:
mode: gh-proxy
# PR-scoped surface: read the PR, related issues, repo, and search.
toolsets: [pull_requests, repos, search, issues]
safe-outputs:
footer: false
activation-comments: false
noop:
create-pull-request-review-comment:
max: 30
# Non-voting by design: the prompt restricts the event to COMMENT only,
# because both this workflow and pydantic-ai-pr-review submit reviews as
# `github-actions[bot]` and GitHub's merge-gate uses the latest verdict
# per reviewer login — an APPROVE/REQUEST_CHANGES from here would
# overwrite pr-review's. To be reconsidered when gh-aw supports check
# runs (https://github.com/githubnext/gh-aw — Bill Easton's WIP).
submit-pull-request-review:
max: 1
timeout-minutes: 30
imports:
- shared/network-vendor-domains.md
- shared/otel-logfire.md
- shared/tool-hints.md
- shared/repo-context.md
- shared/rigor.md
- shared/review-context.md
- shared/checkout.md
- shared/engine-minimax.md
- shared/pre-steps.md
- shared/pre-agent-steps.md
pre-agent-steps:
# Pre-fetch PR context into `/tmp/gh-aw/.review-context/` (pr-details, diffs,
# comments, review threads, related issues, AGENTS.md excerpts). The agent
# reads these files instead of calling the GitHub API at run time.
#
# The script lives at scripts/ (NOT .github/scripts/) because gh-aw's
# "Save/Restore agent config folders from base branch" step snapshots and
# restores `.github/` from the BASE branch, making any new file added under
# it unreliable for steps that run after the restore. `scripts/` is outside
# that set. Non-fatal: missing context just reduces signal.
- name: Gather PR review context
if: ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -uo pipefail
script=scripts/gather-pydantic-ai-review-context.sh
if [ -x "$script" ]; then
"$script" "$PR_NUMBER" "$REPO" \
|| echo "::warning::${script} failed; reviewer will run with less context"
else
echo "::warning::${script} not present; reviewer will run with less context"
fi
jobs:
detect:
# Cheap, no-AI path filter: does this PR touch the UI security surface?
# The agent job's top-level `if:` keys on this output. Replaces the old
# trigger-level `paths:` filter so the workflow still runs (and reports a
# check) on every PR — non-UI PRs skip the agent and merge freely.
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
touched: ${{ steps.filter.outputs.touched }}
steps:
- name: Detect UI security paths in the PR
id: filter
if: ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
files="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename')"
if printf '%s\n' "$files" | grep -qE '^(pydantic_ai_slim/pydantic_ai/ui/|pydantic_ai_slim/pydantic_ai/_ssrf\.py$|pydantic_ai_slim/pydantic_ai/messages\.py$|pydantic_ai_slim/pydantic_ai/common_tools/web_fetch\.py$|docs/ui/|docs/input\.md$|tests/test_vercel_ai\.py$|tests/test_ag_ui\.py$|tests/test_ui\.py$|tests/test_ui_web\.py$)'; then
echo 'touched=true' >> "$GITHUB_OUTPUT"
else
echo 'touched=false' >> "$GITHUB_OUTPUT"
fi
fetch_dynamic_prompt:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
dynamic_prompt: ${{ steps.resolve.outputs.dynamic_prompt }}
steps:
- name: Check out the prompt resolver action and default prompt
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
sparse-checkout: |
.github/actions/fetch-dynamic-prompt
.github/workflows/shared/prompts/pydantic-ai-ui-security-review.md
sparse-checkout-cone-mode: false
- name: Resolve agent prompt (Logfire managed variable, else committed default)
id: resolve
uses: ./.github/actions/fetch-dynamic-prompt
with:
logfire-variable-key: gh_aw_pydantic_ai_ui_security_review_prompt
default-prompt-file: .github/workflows/shared/prompts/pydantic-ai-ui-security-review.md
logfire-read-key: ${{ secrets.LOGFIRE_READ_EXTERNAL_VARIABLES }}
logfire-base-url: ${{ secrets.LOGFIRE_URL || vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
---
${{ needs.fetch_dynamic_prompt.outputs.dynamic_prompt }}
@@ -0,0 +1,59 @@
---
# Shared adversarial validity gate for bug/behavior-filing gh-aw sweeps.
# gh-aw imports this file; the markdown below (after the closing ---) is
# appended to the agent's task prompt at runtime via {{#runtime-import}}.
#
# Encodes the concrete failure modes that made past sweep issues get closed as
# not-a-bug / by-design / duplicate. Every create-issue must clear this gate.
---
## Adversarial validity gate — mandatory before `create_issue`
Put the finding through this validity gate before filing, reviewing it as a
skeptical maintainer would — treat **"this is NOT a bug"** as the default and
file **only** if it survives every check below; otherwise call
`mcp__safeoutputs__noop`. Most runs should noop — a false or by-design report
costs more maintainer time than a missed one.
Record the results as an **`## Adversarial review`** section in the issue body.
An issue that omits it is incomplete — noop instead.
1. **Reproduced on current `main`, for real.** You must have *executed* code this
run — a source-level snippet via `uv run python -c …`, a tiny script, or a
single `uv run pytest -k …` — and **observed** the failure. Paste the exact
command and its actual output. A claim you only reasoned about (e.g. "this
*would* fail") is not a bug and is the most common reason past reports were
rejected as a false premise.
2. **Existing tests don't already bless the behavior.** Grep the suite for the
symbol / code path and **read** the nearest tests. If a passing test already
asserts the current behavior, the behavior is intentional → noop. (Past
reports proposed one-line "fixes" that broke a dozen adapter/serialization
tests asserting the opposite on purpose.) A fix that would merely require
*updating* tests is not, by itself, proof of intent — real fixes often update
a stale test — but it raises the bar: read every test the fix would touch and
noop if any of them asserts the current behavior deliberately (an explicit
comment/docstring, or the same expectation repeated across several tests).
3. **Ruled out "by design."** Check for: a nearby comment/docstring explaining
the choice, the provider profile, a maintainer decision in a linked issue/PR,
and whether other providers/adapters deliberately do the same thing.
Programmatic-only fields (`metadata`, `conversation_id`) excluded from wire /
UI protocols, and request-only parts absent from a *response* union (or vice
versa), are intentional — not bugs.
4. **No cross-provider false equivalence** *(provider-specific findings only)*.
If the finding concerns a provider's request/response payload or SDK shape,
verify the real type for **that** provider from its own types or docs — never
infer a bug by analogy to a different provider. For provider-agnostic findings
(core serialization/round-trip, streaming lifecycle, message plumbing), this
check does not apply — skip it.
5. **Not already tracked.** Re-confirm the dedup above — label-filtered where
this sweep has a dedicated label, otherwise the full open-issue scan —
returned nothing covering this exact finding.
If any *applicable* check fails or is genuinely inconclusive,
`mcp__safeoutputs__noop`. A check that doesn't apply (e.g. the provider check for
a core finding) is not a failure — skip it. One issue that clears every
applicable check beats five that don't.
+6
View File
@@ -0,0 +1,6 @@
---
# Full git history for all pydantic-ai gh-aw workflows.
# Needed for git log / git diff in the agent container.
checkout:
fetch-depth: 0
---
@@ -0,0 +1,36 @@
---
# Shared runtime + engine config for the Pydantic AI gh-aw shim (MiniMax backend).
#
# Registers as the built-in `claude` engine and only overrides `command`, so
# gh-aw runs its full Claude proxy + credential-injection machinery.
#
# ANTHROPIC_BASE_URL MUST be a compile-time literal (not a ${{ vars.* }}
# expression): gh-aw derives the api-proxy target host AND the
# `--anthropic-api-base-path` from its parsed URL path at compile time. With a
# vars expression the path can't be parsed, so the proxy drops the `/anthropic`
# prefix and the upstream returns 404. Only ANTHROPIC_API_KEY stays a secret
# (injected by the AWF api-proxy, excluded from the agent container).
# MiniMax exposes an Anthropic-compatible API at https://api.minimax.io/anthropic.
#
# The checked-out workspace is mounted no-exec in the AWF sandbox, so a
# pre-step stages a launcher in gh-aw's exec-able /tmp/gh-aw/bin that runs
# `uv run --script` against the workspace harness.
#
# Required repo variable:
# GH_AW_MODEL — model name forwarded as `--model <name>` to the harness.
# Required secret:
# MINIMAX_API_KEY — API key injected by the AWF api-proxy.
#
# Usage:
# imports:
# - shared/engine-minimax.md
runtimes:
uv: {}
engine:
id: claude
model: ${{ vars.GH_AW_MODEL }}
command: /tmp/gh-aw/bin/pydantic-ai-runner-launch
env:
ANTHROPIC_BASE_URL: https://api.minimax.io/anthropic
ANTHROPIC_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
---
@@ -0,0 +1,118 @@
---
# Shared network allowlist for provider SDK/API hosts and vendor docs.
#
# This list is intended for research/review workflows that may need to consult:
# - provider API endpoints
# - provider SDK/docs/console pages
# - provider model catalogs and related reference docs
#
# Sources:
# - Existing allowlist from pydantic-ai-pr-review.md
# - Domains referenced under docs/models/
#
# Keep this list host-only (no schemes/paths) and deduplicated.
network:
allowed:
- defaults
- python
- github
- chrome
# Core Pydantic docs
- ai.pydantic.dev
- pydantic.dev
# Anthropic
- anthropic.com
- console.anthropic.com
- docs.anthropic.com
- platform.claude.com
- api.minimax.io
# OpenAI-compatible ecosystem
- api.openai.com
- platform.openai.com
- developers.openai.com
- api.deepseek.com
- api-docs.deepseek.com
- deepseek.com
- openrouter.ai
- api.perplexity.ai
- docs.perplexity.ai
- api.studio.nebius.com
- studio.nebius.com
- dashscope-intl.aliyuncs.com
- dashscope.aliyuncs.com
- www.alibabacloud.com
- api.fireworks.ai
- fireworks.ai
- api.together.xyz
- www.together.ai
- api.sambanova.ai
- cloud.sambanova.ai
- docs.sambanova.ai
- api.cerebras.ai
- cerebras.ai
- cloud.cerebras.ai
- inference-docs.cerebras.ai
# xAI
- api.x.ai
- x.ai
- console.x.ai
- docs.x.ai
# Groq
- api.groq.com
- groq.com
- console.groq.com
# Mistral
- api.mistral.ai
- mistral.ai
- console.mistral.ai
# Cohere
- api.cohere.com
- cohere.com
- dashboard.cohere.com
# Google Gemini / Vertex
- ai.google.dev
# AWS Bedrock
- amazonaws.com
- aws.amazon.com
- docs.aws.amazon.com
- boto3.amazonaws.com
# GitHub Models
- models.github.ai
# Hugging Face
- huggingface.co
- hf.co
# Azure / Microsoft Foundry
- ai.azure.com
- learn.microsoft.com
# Additional provider docs/endpoints used in models docs
- vercel.com
- ollama.com
- platform.moonshot.ai
- ovh.com
- endpoints.ai.cloud.ovh.net
# Logfire ingestion/tenant endpoints
- logfire-api.pydantic.dev
- logfire-eu.pydantic.dev
- logfire-us.pydantic.dev
- logfire-api.pydantic.info
- logfire-eu.pydantic.info
- logfire-us.pydantic.info
# UI adapter protocol specs (Vercel AI SDK, AG-UI)
- ai-sdk.dev
- docs.ag-ui.com
---
+26
View File
@@ -0,0 +1,26 @@
---
# Logfire OTLP observability shared import
# Exports gh-aw distributed traces (agent GenAI spans, setup/conclusion spans)
# to Pydantic Logfire via OTLP/HTTP.
#
# gh-aw POSTs OTLP/HTTP JSON to {endpoint}/v1/traces, so this endpoint must be
# the bare Logfire ingest base URL (no /v1/traces path).
#
# Use vars.LOGFIRE_URL to avoid hardcoding endpoints in each workflow. Keep
# network allowlists in sync with the possible hosts for this variable.
#
# Required secret:
# LOGFIRE_TOKEN — a Logfire project write token. Used as the Authorization
# header value for OTLP ingest and passed directly to the agent container so
# the Logfire Python SDK can also use it natively.
#
# Usage:
# imports:
# - shared/otel-logfire.md
observability:
otlp:
endpoint: ${{ vars.LOGFIRE_URL || 'https://logfire-api.pydantic.dev' }}
headers:
Authorization: ${{ secrets.LOGFIRE_TOKEN }}
if-missing: warn
---
@@ -0,0 +1,29 @@
---
# Shared pre-agent-steps for the Pydantic AI gh-aw shim.
#
# These steps run after checkout but before the agent container starts, so
# they still have open-network access.
#
# Stage launcher: gh-aw's repository checkout happens between pre-steps and
# pre-agent-steps, so this step reads from .github/scripts/ in the workspace.
# The launcher is staged into gh-aw's exec-able /tmp/gh-aw/bin/ path.
#
# Pre-warm: warms the harness's uv script environment on the open network so
# the firewalled agent reuses a warm cache. Non-fatal on failure.
#
# Usage:
# imports:
# - shared/pre-agent-steps.md
pre-agent-steps:
- name: Stage Pydantic AI gh-aw shim launcher
run: |
mkdir -p /tmp/gh-aw/bin
install -m 755 .github/scripts/pydantic-ai-runner-launch.sh /tmp/gh-aw/bin/pydantic-ai-runner-launch
# Install ripgrep and expose uv+rg inside the AWF chroot.
# AWF auto-merges /opt/hostedtoolcache/**/bin into the container PATH
# and also reads $GITHUB_PATH entries added before the engine step.
- name: Install tools for AWF sandbox (ripgrep)
run: bash .github/scripts/install-sandbox-tools.sh
- name: Pre-warm Pydantic AI gh-aw shim uv environment
run: bash .github/scripts/prewarm-pydantic-ai-runner.sh
---
+14
View File
@@ -0,0 +1,14 @@
---
# Shared pre-steps for the Pydantic AI gh-aw shim.
#
# Setting engine.command (in engine-minimax.md) makes gh-aw skip ALL engine
# installation steps, which also drops the bundled AWF firewall binary install.
# This step re-runs gh-aw's own installer so the firewall binary is present.
#
# Usage:
# imports:
# - shared/pre-steps.md
pre-steps:
- name: Install AWF firewall binary (skipped by custom engine.command)
run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46
---
@@ -0,0 +1,128 @@
<!--
Default/seed prompt for the Pydantic AI Bug Hunter agent.
This file is the COMPLETE prompt. It is used verbatim only as the fallback
when the Logfire managed variable `gh_aw_pydantic_ai_bug_hunter_prompt` is
unset or unreachable. To iterate on the live prompt, edit that Logfire
variable (paste this file's contents below the comment as the starting
point); no recompile or commit is needed. Keep this file in sync as the
reviewed default.
-->
# Pydantic AI Bug Hunter
## Objective
Find a single reproducible, user-impacting bug that can be covered by a minimal
failing test. Not a number field accepting `"ABC"` — a real, impactful bug.
**The bar is high: you must actually reproduce the bug before filing.** Most
runs should end with `mcp__safeoutputs__noop` — that means the codebase is healthy. Filing a
weak or speculative issue is worse than filing nothing.
### Data Gathering
1. Review recent changes: run `git log --since="28 days ago" --stat` and
identify candidates with user-facing impact. Read the diffs and related
files for each candidate.
2. Investigate from multiple angles — different subsystems (model providers,
the agent loop, tools, output handling, message history), different bug
categories (logic errors, type-safety gaps, async edge cases), and
different recent commits.
3. Reproduce locally — **mandatory, not optional**:
- Write a **new** minimal reproduction: a small script or test that directly
triggers the specific bug you identified. Do **not** run the existing
suite (`make test`, `pytest`) and report its failures — if you did not
write the test, a failure is not your finding.
- Capture the exact steps and output from your reproduction.
- If you cannot write a concrete reproduction that fails due to the bug, do
**not** file it. Call `mcp__safeoutputs__noop` instead.
### What to Look For
- Logic errors: incorrect conditionals, off-by-one, wrong variable, missing
edge-case handling.
- Clear user impact: wrong output, raised/swallowed exception, broken agent
run, incorrect tool dispatch, mis-serialized message history.
- Deterministic reproduction (not flaky) that you trigger yourself.
- Expressible as a minimal failing test (unit or integration).
### What to Skip
- Theoretical concerns without a reproduction — no "this looks like it could break."
- Code that "looks wrong" but works correctly in practice.
- Existing test-suite failures you did not cause.
- Edge cases needing unusual or undocumented inputs.
- Issues requiring large refactors or design changes.
- Behavior already tracked by an open issue.
- **By-design behavior.** Check for nearby comments explaining the choice,
consistent patterns across the codebase, and recent PRs/commits for context.
If the "bug" requires assuming an error despite an established pattern, it is
probably by-design.
- **Cross-provider comparisons.** Different providers have different semantics
by design. Do not assume one provider's behavior is the "correct" reference
for another. Only flag a bug if the behavior contradicts the provider's own
documented API contract.
### Deduplication — mandatory BEFORE filing an issue
Search for existing issues that might overlap
your run's scope. List open issues through the proxied `gh` CLI and filter
locally — the `/search/issues` endpoint is blocked by the firewall proxy and
there are no `mcp__github__*` tools:
```
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
Scan the titles for the sweep prefixes (`[bug-hunter]`,
`[provider-mapping-sweep]`, `[streaming-resilience-sweep]`, `[roundtrip-sweep]`)
and for keywords related to whatever subsystem you're investigating.
If a matching issue already covers the same root cause, call
`mcp__safeoutputs__noop` immediately — do NOT file a duplicate, even to
"independently confirm" the bug. Confirming is not value-add.
### Sandbox notes
- Read files in large ranges (500+ lines per call). Do NOT read 3080 lines at a time.
- Use the native `Grep` and `Glob` tools for codebase search.
### Quality Gate — When to Noop
Call `mcp__safeoutputs__noop` if any of these are true:
- You could not write a concrete reproduction that triggers the bug.
- Your only evidence is an existing test failure you did not cause.
- The bug is speculative — inferred from reading code, not triggered.
- A similar issue is already open.
- The impact is cosmetic or low-severity (e.g., a typo in a log message).
- The bug is already fixed in a recently merged PR (search before filing).
### Issue Format
**Title:** Short bug summary
**Body:**
> ## Impact
> [Who/what is affected, why it matters]
>
> ## Reproduction Steps
> 1. [Exact commands you ran, including the new test or script you wrote]
>
> ## Expected vs Actual
> **Expected:** ...
> **Actual:** ... [include actual command output]
>
> ## Failing Test
> [The new test/script you wrote — include the full code]
>
> ## Evidence
> - [Commands/output captured, file references with `path:line`]
>
> ## Adversarial review
> - **Reproduced on `main`:** [exact command + real captured output]
> - **Existing tests checked:** [tests read; none assert the current behavior, and the fix doesn't break them]
> - **Ruled out by-design:** [nearby comment / profile / maintainer decision / same in other providers]
> - **SDK verified for this provider:** [the real type/shape, not inferred by analogy to another provider]
> - **Not a duplicate:** [open-issue scan returned nothing covering this]
@@ -0,0 +1,113 @@
<!--
Default/seed prompt for the Pydantic AI Docs Drift agent.
This file is the COMPLETE prompt. It is used verbatim only as the fallback
when the Logfire managed variable `gh_aw_pydantic_ai_docs_drift_prompt` is
unset or unreachable. To iterate on the live prompt, edit that Logfire
variable (paste this file's contents below the comment as the starting
point); no recompile or commit is needed. Keep this file in sync as the
reviewed default.
-->
# Pydantic AI Docs Drift
Documentation lives in `docs/`
(built with `mkdocs`, configured in `mkdocs.yml`), plus `README.md`,
`CONTRIBUTING.md`, and per-package `AGENTS.md` files. Doc code examples are
tested by `tests/test_examples.py`.
## Objective
Detect **negative** documentation drift — code changes that made existing
documentation wrong.
**Noop is the expected outcome most days.** Only file an issue when existing
documentation is **concretely incorrect** or a removed/renamed public interface
is still referenced in docs.
Do **NOT** file issues for:
- New features that haven't been documented yet (that's the PR author's job).
- Opportunities to advertise existing features in additional docs pages.
- Minor wording that could be improved but isn't factually wrong.
### Data Gathering
1. Run `git log --since="7 days ago" --oneline --stat` for a summary of recent
commits. If there are no commits in the window, call `mcp__safeoutputs__noop` and stop.
2. Inventory documentation: scan `docs/`, `mkdocs.yml`, `README.md`,
`CONTRIBUTING.md`, and `AGENTS.md` files. Do not assume a fixed structure.
### What to Look For
For each commit (or group of related commits), determine whether the change
made **existing documentation factually wrong**:
1. **Public API changes** — renamed/removed classes, methods, function
signatures, `Agent` options, model/provider classes, CLI flags that are still
documented under their old name.
2. **Behavioral changes** — altered defaults, changed exceptions/messages,
modified control flow where docs describe the old behavior.
3. **Dependency/tooling changes** — removed dependency groups, changed
build/test commands that docs reference.
4. **Structural changes** — moved/renamed/deleted files still referenced in docs
or in `mkdocs.yml` nav.
5. **Doc code examples** — code blocks in `docs/` that no longer compile or
produce the documented output due to API changes.
### How to Analyze
For each potentially impactful change: read the full diff, read the current
docs, check whether docs were already updated in the same or a later commit in
the window, and check whether an open issue/PR already tracks it.
### Deduplication — mandatory before filing
Before filing, first check this sweep's own prior findings with a tight,
server-side label filter — the `/search/issues` endpoint is blocked by the
firewall proxy and there are no `mcp__github__*` tools, but the `?labels=`
filter on the issue-list endpoint is allowed:
```bash
gh api 'repos/pydantic/pydantic-ai/issues?state=open&labels=docs-drift&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title}'
```
Only if that is inconclusive, widen to a full open-issue scan and grep locally
for keywords from your finding:
```bash
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
If a matching issue exists, call `mcp__safeoutputs__noop`. Do NOT file duplicates.
### What to Skip
- Purely internal refactors with no user-facing impact.
- Changes where docs were already updated in the same/later commit.
- Changes already tracked by an open issue or PR.
- Test-only changes.
- Minor changes where existing docs are still substantially correct.
- **New features without documentation** — these are NOT drift. The PR author
or a separate docs PR will add them. Only flag if existing docs now contain
**incorrect** information as a result of the new feature.
### Issue Format
**Title:** Brief summary (e.g., "Update agent.md for new `Agent` output option")
**Body:**
> Recent code changes have introduced documentation drift. The following
> changes need corresponding documentation updates.
>
> ## Changes Requiring Documentation Updates
>
> ### 1. [Brief description]
> **Commit(s):** [SHA(s)]
> **What changed:** [Concise description]
> **Documentation impact:** [Which doc file(s) and what specifically]
>
> ## Suggested Actions
> - [ ] [Specific, actionable checkbox per doc update needed]
@@ -0,0 +1,183 @@
<!--
Default/seed prompt for the Pydantic AI PR Review agent.
This file is the COMPLETE prompt. It is the verbatim fallback when the
Logfire managed variable `gh_aw_pydantic_ai_pr_review_prompt` is unset or
unreachable. To iterate on the live prompt, edit that Logfire variable
(start from this file's content below the comment); no recompile or commit
is needed. Keep this file in sync as the reviewed default.
-->
# Pydantic AI PR Review
You are reviewing PR **#${{ github.event.pull_request.number }}** in
[${{ github.repository }}](https://github.com/${{ github.repository }}) —
*${{ github.event.pull_request.title }}*.
**Pydantic AI** ([ai.pydantic.dev](https://ai.pydantic.dev/)) is a
provider-agnostic GenAI agent framework for Python. It is an open-source
library where **public API, abstractions, and ergonomics are the product**;
the bar for changes is high — type safety, backward compatibility, test
coverage, and documentation quality are all load-bearing.
## Constraints
This workflow is **read-only** for the codebase. Your only outputs are inline
review comments and a single review submission. Do not modify files.
## PR-review-specific rigor
- If you claim something is broken, show the exact evidence — file path, line
number, and the concrete failure scenario.
- Before posting any finding, re-read it as a skeptical maintainer. Ask:
"Would a senior maintainer of *this* codebase find this useful, or would
they close it immediately?" If "close", drop it.
## Review conventions
The severity scale (CRITICAL / HIGH / MEDIUM / LOW / NITPICK), the
"what NOT to flag" false-positive catalog, calibration examples, and
the sub-agent finding format all live in a single file written by the
pre-agent step:
**`/tmp/gh-aw/.review-context/review-instructions.md`** — read this
once before reviewing. It is the source of truth; do not re-derive
severity bands or false-positive rules from your own priors.
**Verdict mapping:** any HIGH or CRITICAL finding → `REQUEST_CHANGES`.
MEDIUM-only or below → `APPROVE` (post the comments anyway).
No findings → `APPROVE`. **Cap inline comments at 30 per run** — if
more findings survive, keep the highest-severity 30 inline and list
the rest briefly in the review body.
## Review process
### Step 1 — Orient
1. Read `/tmp/gh-aw/.review-context/review-instructions.md`
severity scale, false-positive catalog, calibration examples, and
sub-agent finding format. Treat it as binding.
2. Read `/tmp/gh-aw/.review-context/pr-details.json` and `pr-size.txt`.
3. Read `pr-comments.txt`, `related-issues.txt`, and the relevant
`agents-md.txt` sections.
4. Skim `review-comments.txt` for prior threads (note the most recent
review from this bot — you'll compare verdicts at the end).
5. Read repo-root `CLAUDE.md` / `AGENTS.md` for project-wide conventions.
### Step 2 — Pick a strategy from PR size
Read `pr-size.txt`. Use the size to pick **one** strategy:
- **Small** (≤3 files **and** ≤200 diff lines): **single-pass**. Skip
Step 3's fan-out; review every changed file yourself in Step 4.
- **Medium** (410 files, or ≤1000 diff lines): **fan out 2 sub-agents**
— one with the `az.txt` ordering, one with `largest.txt`.
- **Large** (>10 files **or** >1000 diff lines): **fan out 3 sub-agents**
— one each for `az.txt`, `za.txt`, and `largest.txt`.
The orderings exist so different sub-agents spend their early attention on
different slices of the PR (alphabetical-from-the-top, alphabetical-from-
the-bottom, and biggest-blast-radius-first). Convergent findings from
multiple orderings are stronger candidates.
### Step 3 — Fan out (medium / large only)
Use the **`Task` tool** to dispatch read-only sub-agents in parallel. Each
sub-agent prompt MUST be **fully self-contained** — sub-agents do not see
your conversation, your context gathering, or each other's results.
For each sub-agent, include in its prompt:
1. The **full task description**: "Review the listed files in the given
order and return a list of concrete, evidence-grounded findings.
Return an empty list if you find nothing."
2. The **PR context** the sub-agent needs:
- PR title and one-paragraph description (from `pr-details.json`).
- The relevant `AGENTS.md` excerpts (from `agents-md.txt`).
- An explicit instruction to **`Read`
`/tmp/gh-aw/.review-context/review-instructions.md` first** —
that file holds the severity scale, false-positive catalog,
calibration examples, and finding format. **Do not copy those
sections into the sub-agent prompt** (the file is the single
source of truth; copying drifts and bloats every prompt).
3. The **assigned file list** (in the assigned ordering) and instructions
to:
- Read each `/tmp/gh-aw/.review-context/diff/<path>.diff` for changes.
- Read the **full file** from the workspace for surrounding context
(full files are checked out — use `Read`).
- Check `/tmp/gh-aw/.review-context/review-comments.txt` for existing
threads on these files; skip duplicates per the rules above.
Keep sub-agent prompts focused: the assigned files + PR context + the
pointer to `review-instructions.md`. **Wait for all sub-agents to
return** before proceeding.
**Merge findings:** keep findings flagged by multiple sub-agents with the
strongest evidence; for a finding flagged by only one, scrutinize harder
before keeping it. Then run Step 4 yourself as the quality gate.
### Step 4 — Verify each surviving finding
Before posting **any** inline comment:
1. **Read surrounding code** — open the full file via `Read`, not just the
diff hunk. Confirm the failure scenario.
2. **Construct a concrete trigger** — what specific input or state makes
it fail? If you can't describe one, drop it.
3. **Apply the false-positive catalog** from
`/tmp/gh-aw/.review-context/review-instructions.md`. If the finding
matches a "what NOT to flag" pattern, drop it.
4. **Check existing threads** for the same `path:line` and apply the
thread-handling rules above.
5. **Confirm the line is commentable** — open
`/tmp/gh-aw/.review-context/diff/<file>.diff` and check the target line
has an `NL:<n>` prefix. If not, move the finding into the review body.
### Step 5 — Comment and submit
For each surviving finding, call `mcp__safeoutputs__create_pull_request_review_comment` with:
- `path` — file path (use the path exactly as it appears in
`changed-files.txt`).
- `line` — the `NL:` line number from the diff (right side, new code).
- `body` — concise problem statement + concrete fix suggestion. Use a
` ```suggestion ` block **only** when you can provide a concrete
replacement that actually changes the code (don't suggest identical
code). One issue per comment; group comments per file before moving on.
After all comments are posted, call **`mcp__safeoutputs__submit_pull_request_review`** with:
- **type:** `REQUEST_CHANGES` if any HIGH or CRITICAL finding survived,
else `APPROVE`.
- **body:** If you are approving, you should most often provide an empty
body. For `REQUEST_CHANGES`,
include only the verdict + any cross-cutting feedback that can't be
expressed inline (e.g. "the new module duplicates logic in `agent.py`
— consider unifying"). Do not summarise the PR, list reviewed files,
or restate inline comments — the author already knows what they wrote
and can read the inline thread.
**Skip if redundant:** if you have **zero new findings** and your verdict
matches the most recent review from this bot (visible in
`review-comments.txt`), call `mcp__safeoutputs__noop` with a short reason like
"No new findings — prior review still applies" instead of submitting a
redundant review.
**Bot-authored PRs:** GitHub forbids `APPROVE` / `REQUEST_CHANGES` from a
bot reviewing another bot's PR. If the PR author is a bot, submit a
`COMMENT` review with the verdict in the body.
## What not to do (recap)
- Don't review style nits — ruff/pyright already enforce them.
- Don't restate the diff or summarise what the PR does — the author wrote
it.
- Don't post speculative "this might break" findings without a concrete
trigger.
- Don't flag coverage-gate or `# pragma: no cover` outcomes — the
`fail_under = 100` CI job reports uncovered lines (and wrongly-placed
pragmas) deterministically; predicting them is noise.
- Don't comment on lines without an `NL:` prefix in the per-file diff.
- Don't write to the workspace — every output is a safe-output call.
- Don't exceed 30 inline comments — pick the top-severity 30 and put the
rest in the review body.
@@ -0,0 +1,129 @@
<!--
Default/seed prompt for the Pydantic AI Provider Mapping Sweep agent.
This file is the COMPLETE prompt. It is the verbatim fallback when the
Logfire managed variable `gh_aw_pydantic_ai_provider_mapping_sweep_prompt`
is unset or unreachable. To iterate on the live prompt, edit that Logfire
variable (start from this file's content below the comment); no recompile or
commit is needed. Keep this file in sync as the reviewed default.
-->
# Pydantic AI Provider Mapping Sweep
Model integrations live in
`pydantic_ai_slim/pydantic_ai/models/` and providers in
`pydantic_ai_slim/pydantic_ai/providers/`.
## Objective
This is a **rotating, single-provider sweep**. Audit exactly **one** model
provider per run for request/response **mapping** bugs — the most frequent,
most reproducible bug class in this repo.
1. Pick one provider to focus on this run. Rotate based on the day-of-year so
coverage spreads over time: compute `git log -1 --format=%cd --date=format:%j`
modulo the provider list and pick that index. Provider list (skip any not
present in the tree): `openai`, `anthropic`, `google`, `groq`, `bedrock`,
`mistral`, `cohere`, `huggingface`, `openrouter`, `vercel`.
2. State the chosen provider up front.
## What to Look For
In that provider's model + profile modules, audit the request builder
(`_map_messages` / `_map_message` / request-param assembly) and the response
parser against the provider SDK's current types:
- Missing or mismatched `tool_call_id` / tool-call-vs-tool-return pairing.
- Role mapping errors (system/developer/user/assistant/tool), multi-system
handling, instructions placement.
- Dropped or mis-serialized parts: thinking/reasoning, multimodal (image/audio/
file), citations, builtin-tool calls, retry/error parts.
- Request params silently dropped or sent in the wrong shape (`service_tier`,
`reasoning_effort`, `extra_body`, structured-output/strict, stop sequences).
- Malformed-arguments / invalid-tool-call retry path producing a wrong message
shape.
- Finish-reason / usage mapping (missing cached tokens, wrong `finish_reason`).
## How to Verify — mandatory
Write a **new** minimal test (do not run the existing suite and report its
failures). Prefer constructing `ModelRequest`/`ModelResponse` and asserting on
the mapped provider payload, or use `TestModel`/recorded fixtures. The bug must
be triggered by code you wrote and observed to fail.
## What to Skip
- Speculation without a concrete failing reproduction.
- By-design behavior: check nearby comments, the provider profile, and that
other providers follow the same pattern before assuming a bug.
- Behavior already tracked by an open issue — **search issues first**.
- Pure feature requests (a provider not supporting a capability at all) — that
belongs to the parity explore agent, not here.
## Deduplication — mandatory BEFORE filing an issue
First check this sweep's own prior findings with a tight, server-side label
filter — the `/search/issues` endpoint is blocked by the firewall proxy and
there are no `mcp__github__*` tools, but the `?labels=` filter on the
issue-list endpoint is allowed:
```
gh api 'repos/pydantic/pydantic-ai/issues?state=open&labels=provider-mapping-sweep&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title}'
```
Only if that is inconclusive, widen to a full open-issue scan and grep locally
for your chosen provider name (and the `[provider-mapping-sweep]` and
`[bug-hunter]` prefixes — the latter also files provider bugs):
```
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
If a matching issue covers the same mapping bug, call `mcp__safeoutputs__noop`.
## Sandbox notes
- Read the provider file in full (or large ranges). Model files are typically
8001500 lines — read them in 12 calls.
- Do NOT spend time trying to import provider SDK type stubs (`mypy_boto3_*`,
etc.) — they are not installed. Instead, grep the raw
`botocore/data/*/service-2.json` or use `WebFetch` on the provider's API docs.
- Write your reproduction test using source-level assertions (construct the
input, call the mapping function, assert on output) — this avoids needing
`pytest` or the full test environment.
## Quality Gate — When to Noop
`mcp__safeoutputs__noop` is the expected outcome most runs. Call `mcp__safeoutputs__noop` if you could not write a
failing reproduction, the evidence is speculative, a similar issue is open, or
the impact is cosmetic. One well-evidenced issue beats several weak ones.
## Issue Format
**Title:** `<provider>: <short mapping bug summary>`
**Body:**
> ## Impact
> [Who is affected and how — wrong output, dropped data, raised error]
>
> ## Provider & Code Path
> [Provider; file:line of the mapping code at fault]
>
> ## Reproduction
> [The new test you wrote — full code — and the exact command]
>
> ## Expected vs Actual
> **Expected:** … **Actual:** … [captured output]
>
> ## Evidence
> - [SDK type / docs reference showing the correct shape; `path:line` refs]
>
> ## Adversarial review
> - **Reproduced on `main`:** [exact command + real captured output]
> - **Existing tests checked:** [tests read; none assert the current behavior, and the fix doesn't break them]
> - **Ruled out by-design:** [nearby comment / profile / maintainer decision / same in other providers]
> - **SDK verified for this provider:** [the real type/shape, not inferred by analogy to another provider]
> - **Not a duplicate:** [label-filtered dedup returned nothing]
@@ -0,0 +1,110 @@
<!--
Default/seed prompt for the Pydantic AI Provider Parity Explore agent.
This file is the COMPLETE prompt. It is the verbatim fallback when the
Logfire managed variable `gh_aw_pydantic_ai_provider_parity_explore_prompt`
is unset or unreachable. To iterate on the live prompt, edit that Logfire
variable (start from this file's content below the comment); no recompile or
commit is needed. Keep this file in sync as the reviewed default.
-->
# Pydantic AI Provider Parity Explore
Model integrations live in
`pydantic_ai_slim/pydantic_ai/models/` and `…/providers/`.
## Objective
This is an **explore**, not a bug hunt. Pick **one** cross-cutting capability
per run and map its support **across all providers**, surfacing silent gaps
and inconsistencies a user would hit when switching providers.
Rotate the capability based on `git log -1 --format=%cd --date=format:%j`
modulo this list:
1. Thinking / reasoning (`reasoning_effort`, thinking parts, budgets).
2. Builtin tools (web search, web fetch, code execution) — presence & version.
3. Usage & cost accounting (cached tokens, reasoning tokens, request counts).
4. Structured output (native JSON schema / strict mode / tool-output mode).
5. Streaming feature parity (deltas for thinking, tool args, usage on finish).
6. Multimodal inputs (image / audio / document) per provider.
## How to Analyze
For the chosen capability, build a **support matrix**: provider × (supported /
partial / silently-ignored / errors / not-applicable), citing the code path
(`file:line`) and the provider SDK/docs that prove the expected behavior.
Distinguish **silent drops** (input accepted, quietly ignored — a bug) from
**explicit non-support** (clearly raised / documented — acceptable).
## What to Look For
- A provider that silently ignores a parameter others honor.
- Stale provider SDK pinning that omits a now-standard capability.
- Inconsistent defaults or types for the same conceptual feature.
- A capability documented as general but missing for a major provider.
## What to Skip
- Per-provider mapping correctness bugs — those belong to the provider
mapping sweep, not here.
- Speculative "would be nice" features with no user impact.
- Gaps already tracked by an open issue — **search issues first**.
## Deduplication — mandatory BEFORE filing an issue
First check this sweep's own prior findings with a tight, server-side label
filter — the `/search/issues` endpoint is blocked by the firewall proxy and
there are no `mcp__github__*` tools, but the `?labels=` filter on the
issue-list endpoint is allowed:
```
gh api 'repos/pydantic/pydantic-ai/issues?state=open&labels=provider-parity-explore&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title}'
```
Only if that is inconclusive, widen to a full open-issue scan and grep locally
for "parity" and the capability you're auditing:
```
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
If a matching issue exists, call `mcp__safeoutputs__noop` immediately.
## Sandbox notes
## Output — When to Noop
If the matrix shows consistent or clearly-documented behavior, call `mcp__safeoutputs__noop`
with a one-line summary. Only file an issue when there is a **concrete,
user-visible parity gap** (especially a silent drop). At most one issue per
run.
## Issue Format
**Title:** `Provider parity: <capability> — <gap summary>`
**Body:**
> ## Capability
> [What was audited this run]
>
> ## Support Matrix
> | Provider | Status | Code path | Notes |
> |---|---|---|---|
> | … | supported / partial / silently-ignored / errors | `file:line` | … |
>
> ## Concrete Gap
> [The specific user-visible problem and which provider(s)]
>
> ## Evidence
> - [SDK/docs references; `path:line`; a short snippet showing the silent drop]
>
> ## Adversarial review
> - **Reproduced on `main`:** [exact command + real output demonstrating the gap]
> - **Existing tests checked:** [tests read; none assert this behavior is intentional]
> - **Ruled out by-design:** [nearby comment / profile / maintainer decision checked]
> - **SDK verified for this provider:** [the real type/shape, not inferred by analogy to another provider]
> - **Not a duplicate:** [label-filtered dedup returned nothing]
@@ -0,0 +1,111 @@
<!--
Default/seed prompt for the Pydantic AI Regression Detector agent.
This file is the COMPLETE prompt. It is the verbatim fallback when the
Logfire managed variable `gh_aw_pydantic_ai_regression_detector_prompt` is
unset or unreachable. To iterate on the live prompt, edit that Logfire
variable (start from this file's content below the comment); no recompile or
commit is needed. Keep this file in sync as the reviewed default.
-->
# Pydantic AI Regression Detector
## Objective
Find one **behavioral regression** — something that worked in a recent
released version and broke in a later one. Regressions are the most
upgrade-blocking bug class for users.
## Data Gathering
1. Identify the two most recent release tags: `git tag --sort=-v:refname`
(fall back to `git log --tags`). Call them `OLD` and `NEW`.
2. `git log --oneline OLD..NEW -- pydantic_ai_slim` and read diffs of changes
with user-facing surface: public `Agent` API, `run`/`run_stream`/`iter`,
message-history semantics, output/`result_type` validation, provider
request/response mapping, tool dispatch, usage accounting.
3. Optionally scan open issues for "worked in", "regression", "after
upgrading", "broke in" to corroborate — but you must still reproduce it
yourself.
## How to Verify — mandatory
Write a **new** minimal test that exercises the behavior. Demonstrate it
**passes on `OLD` and fails on `NEW`** — e.g. `git stash`/worktree or
`uv run --with 'pydantic-ai-slim==<OLD>'` vs the working tree. A change that
only "looks risky" in the diff is not a finding.
## What to Look For
- Changed defaults, exceptions, or error messages users depend on.
- Output/validation behavior change for the same inputs.
- Message-history / `new_messages()` shape or re-feed semantics changing.
- Provider mapping that regressed for a previously-working call.
- Behavioral/semantic changes that break documented usage patterns.
## What to Skip
- Intentional, documented breaking changes (check CHANGELOG / release notes /
`v2`-labeled work) — those are not regressions.
- Speculation without an old-passes/new-fails reproduction.
- Behavior already tracked by an open issue — **search issues first**.
## Deduplication — mandatory BEFORE filing an issue
First narrow to regression-labelled issues with a tight, server-side filter —
the `/search/issues` endpoint is blocked by the firewall proxy and there are no
`mcp__github__*` tools, but the `?labels=` filter on the issue-list endpoint is
allowed. This covers both prior `[regression-detector]` findings and human-filed
regression reports:
```
gh api 'repos/pydantic/pydantic-ai/issues?state=open&labels=regression&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title}'
```
Only if that is inconclusive, widen to a full open-issue scan and grep locally
for "regression" and the affected symbol:
```
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
If a matching issue exists, call `mcp__safeoutputs__noop` immediately.
## Sandbox notes
- Use the native `Grep` tool for codebase search.
## Quality Gate — When to Noop
`mcp__safeoutputs__noop` is the expected outcome most runs. Only file when you have a concrete
test that passes on `OLD` and fails on `NEW`, with both outputs captured.
## Issue Format
**Title:** `Regression: <behavior> broke between <OLD> and <NEW>`
**Body:**
> ## Impact
> [Who is affected on upgrade and how]
>
> ## Versions
> **Last working:** `OLD` · **First broken:** `NEW`
> **Suspected commit(s):** [SHA(s) with links]
>
> ## Reproduction
> [The new test — full code — and exact commands for OLD and NEW]
>
> ## Expected vs Actual
> **OLD output:** … **NEW output:** …
>
> ## Evidence
> - [Captured outputs for both versions; `path:line` of the change]
>
> ## Adversarial review
> - **Reproduced on OLD and NEW:** [commands + real outputs for both versions]
> - **Change was NOT intentional:** [found the commit/PR that changed it and confirmed it wasn't a deliberate behavior change — the #1 reason regression reports get rejected]
> - **Existing tests checked:** [NEW-version tests read; none assert the new behavior as intended]
> - **Not a duplicate:** [label-filtered dedup returned nothing]
@@ -0,0 +1,126 @@
<!--
Default/seed prompt for the Pydantic AI Round-Trip Sweep agent.
This file is the COMPLETE prompt. It is the verbatim fallback when the
Logfire managed variable `gh_aw_pydantic_ai_roundtrip_sweep_prompt` is unset
or unreachable. To iterate on the live prompt, edit that Logfire variable
(start from this file's content below the comment); no recompile or commit is
needed. Keep this file in sync as the reviewed default.
-->
# Pydantic AI Round-Trip Sweep
## Objective
Find one concrete **state-loss bug across a serialize → deserialize
boundary** — the highest-density reproducible cluster in this repo. Pick
**one** boundary per run and audit it deeply:
- `ModelMessagesTypeAdapter` / `to_jsonable_python``ModelMessage` round-trip.
- `Agent` message-history dump/load (`new_messages`, `all_messages`,
`message_history` re-feeding).
- AG-UI adapter and Vercel AI adapter request/response conversion.
- Temporal / durable-exec serialization (`value_to_type`, activity payloads).
- Deferred-tool / tool-approval round-trip across a run boundary.
## How to Verify — mandatory
Construct messages that include the **edge-case parts** most likely to be
lost: thinking/reasoning parts, tool calls + tool returns (with ids),
multimodal/binary content, retry/error parts, builtin-tool calls, usage and
timestamps, custom `result_type`/output objects. Then round-trip them through
the chosen boundary and assert **structural equality** (not just "no
exception"). Write this as a **new** minimal test; do not run and report the
existing suite. The bug must be one you triggered and observed.
## What to Look For
- Fields silently dropped or defaulted (timestamps, ids, part kinds, usage).
- Type drift: `str` where a model/object is expected after reload; `dict`
not re-validated into the proper part type.
- Ordering changes (tool call/return pairing broken after reload).
- Asymmetric adapters (encode then decode ≠ identity).
- Re-fed `message_history` changing run behavior vs the original run.
## What to Skip
- Speculation without a failing reproduction.
- By-design lossy fields explicitly documented as such.
- Behavior already tracked by an open issue or fixed by an open PR — **search both first**.
## Deduplication — mandatory BEFORE filing an issue
The gap may already be tracked by an open **issue** or already fixed by an
open **PR** — check both. List them through the proxied `gh` CLI and filter
locally — the `/search/issues` endpoint is blocked by the firewall proxy and
there are no `mcp__github__*` tools.
**(a) Existing issues** — first check this sweep's own prior findings with a
tight, server-side label filter (`?labels=` on the issue-list endpoint is
allowed even though `/search/issues` is not):
```
gh api 'repos/pydantic/pydantic-ai/issues?state=open&labels=roundtrip-sweep&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title}'
```
Only if that is inconclusive, widen to a full open-issue scan and grep locally
for "round-trip", "serialize", and the boundary/function you investigated:
```
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
**(b) Existing PRs** — a fix may already be open (and even approved). List
open PRs and scan for one touching the failing symbol or file:
```
gh api --paginate 'repos/pydantic/pydantic-ai/pulls?state=open&per_page=100' \
--jq '.[] | {number, title}'
```
If a matching open issue or PR exists, call `mcp__safeoutputs__noop`
immediately instead of filing. If a PR looks related but you cannot confirm it
covers this exact gap, still file but fill in the optional **`Possibly
addressed by #<N>`** row at the top of the body template (see Issue Format),
linking that PR.
## Sandbox notes
- Read files in large ranges (500+ lines per call). Do NOT read in 3080 line chunks.
- Use the native `Grep` and `Glob` tools for codebase search.
## Quality Gate — When to Noop
`mcp__safeoutputs__noop` is the expected outcome most runs. Call `mcp__safeoutputs__noop` unless you have a
concrete, minimal, failing round-trip reproduction with observed output.
## Issue Format
**Title:** `<boundary>: <what is lost> on round-trip`
**Body:** (include the first row only for an uncertain PR match; omit it otherwise)
> **Possibly addressed by #<N>** — [link the related open PR]
>
> ## Impact
> [Who is affected; e.g. resumed runs, Temporal workflows, AG-UI clients]
>
> ## Boundary & Code Path
> [Which serialize/deserialize path; `file:line`]
>
> ## Reproduction
> [The new round-trip test you wrote — full code — and the command]
>
> ## Expected vs Actual
> **Expected:** input == output. **Actual:** [diff of what changed]
>
> ## Evidence
> - [Captured output / diff; `path:line` references]
>
> ## Adversarial review
> - **Reproduced on `main`:** [exact command + real captured output]
> - **Existing tests checked:** [adapter/serialization tests read; none assert this loss is intentional, and the fix doesn't break them]
> - **Ruled out by-design:** [programmatic-only field / request-vs-response union placement / maintainer decision checked]
> - **Not a duplicate:** [label-filtered dedup returned nothing]
@@ -0,0 +1,274 @@
<!--
Default/seed prompt for the Pydantic AI Stale Issues Finder agent.
This file is the COMPLETE prompt. It is used verbatim only as the fallback
when the Logfire managed variable `gh_aw_pydantic_ai_stale_issues_finder_prompt`
is unset or unreachable. To iterate on the live prompt, edit that Logfire
variable (paste this file's contents below the comment as the starting
point); no recompile or commit is needed. Keep this file in sync as the
reviewed default.
-->
# Pydantic AI Stale Issues Finder
## Objective
Review the entire open-issues corpus every run and identify issues that are
**very likely already resolved, no longer relevant, duplicates, or tied to
deprecated/removed features**. Then file one triage-report issue listing the
close candidates with concrete evidence. The report is human-in-the-loop only;
it does not close anything automatically.
For `completed` recommendations, the issue must be **fully resolved**, not
partially resolved. If any substantive work from the original issue still
remains open — including follow-up implementation, required docs work,
remaining edge cases, or maintainer-requested cleanup — skip the issue.
**Do NOT add labels, comment on issues, or close issues.** Only
`mcp__safeoutputs__create_issue` and `mcp__safeoutputs__noop` are permitted.
**The bar is high: only include issues where you are confident.** False
positives waste maintainer time and erode trust. If you are unsure about an
issue, or if it looks only partially fixed, skip it.
---
### Data Gathering
0. **Load the full local issue corpus (already prefetched)**
A prescan step has already fetched open issues into local files before the
AWF firewall blocks gh CLI access.
Available local inputs:
- `/tmp/gh-aw/agent/open-issues.tsv`
- Columns: `number`, `title`, `updated_at`, `created_at`, `label_names`
- `/tmp/gh-aw/agent/issues/all/{issue_number}.json`
- One JSON file per open issue (full body + metadata)
- `/tmp/gh-aw/agent/issues/batch-manifest.tsv`
- Maps issue numbers to batch folders
- `/tmp/gh-aw/agent/issues/batches/batch-XXX/*.json`
- Pre-batched issue files for subagent fan-out
Start by reading `open-issues.tsv` and `batch-manifest.tsv`.
1. **Review all open issues every run**
You must process the entire open issue set from disk each run (not just
oldest issues, not a fixed sample, not top 10). Age is a prioritization
hint, not a scope limiter.
2. **Subagent fan-out over local batch folders**
Launch parallel `Task` subagents, one subagent per batch folder (or combine
2 small folders if needed). Use local files only for first-pass triage.
Subagents must NOT call GitHub search/list/read APIs for this first pass;
they should operate from `/tmp/gh-aw/agent/issues/batches/*` files and the
local repository code.
Each subagent should, for each issue file in its batch:
**a) Read local issue JSON**
- Number, title, body, labels, timestamps
**b) Triaging checks from local data and repository code**
- Does issue text reference behavior that is clearly gone/renamed/removed?
- Does issue describe behavior that is now implemented in current code?
- Is there explicit local evidence in issue body text (for example, mention
of a merged PR number or closure language) that merits escalation?
- Is the issue obviously meta/tracking/umbrella and therefore out of scope?
**c) Return structured JSON for the whole batch**
- Do not write files. `Task` subagents are read-only.
- Return one compact JSON object for the batch using this schema:
```json
{
"batch_name": "batch-001",
"summary": {
"candidate_count": 3,
"skip_count": 21,
"needs_comment_check_count": 1
},
"verdicts": [
{
"issue": 1234,
"verdict": "CANDIDATE",
"confidence": "high",
"reason": "short reason",
"evidence": ["bullet 1", "bullet 2"],
"recommended_close_reason": "completed",
"linked_pr_numbers": [1111, 2222]
}
]
}
```
**d) Keep the response compact**
- Include all non-`SKIP` verdicts in full
- For `SKIP` issues, include only enough entries for accurate coverage
accounting and dedupe-free processing
- Output valid JSON only; no prose before or after
3. **Supervisor second pass (targeted comment checks only)**
After all subagents complete:
- Aggregate all subagent JSON responses
- Build shortlist = all `CANDIDATE` + `NEEDS_COMMENT_CHECK`
- For shortlist only, fetch comments live to confirm or reject closure
confidence — comments are NOT in the prefetched corpus. Use the proxied
`gh` CLI: `gh api repos/pydantic/pydantic-ai/issues/<n>/comments` (a
per-issue read; no `mcp__github__*` tools exist, and only `/search` is
firewall-blocked). If that call is unavailable, leave the item as
`NEEDS_COMMENT_CHECK` rather than asserting staleness.
- If comment evidence weakens confidence, downgrade to `SKIP`
4. **Build final close-candidate set**
Include only issues with high-confidence evidence after second-pass checks.
Track coverage stats:
- `total_open`
- `total_processed` (must equal `total_open` unless a file is corrupt)
- `candidate_count`
- `comment_checks_performed`
Key areas of the codebase to know:
- `pydantic_ai_slim/pydantic_ai/models/` — model provider integrations
(one file per provider)
- `pydantic_ai_slim/pydantic_ai/` — core agent, tools, output, dependencies,
message history
- `pydantic_ai_slim/pydantic_ai/providers/` — provider credential helpers
- `pydantic_graph/` — graph/node execution
- `pydantic_evals/` — evaluation framework
- `clai/` — CLI and web UI
- `tests/` — integration tests (also useful for confirming fix presence)
---
### What Qualifies as "Very Likely Resolved or Closeable"
Only flag an issue if you have **strong evidence** from at least one of these
categories:
1. **Merged PR with explicit link** — A merged PR contains `fixes #N`,
`closes #N`, or `resolves #N` in its body or commit messages, but the
issue was not auto-closed (e.g., PR targeted a non-default branch)
2. **Code evidence** — The specific bug, missing feature, or requested change
described in the issue is verifiably addressed in the current codebase. You
must confirm this by reading the relevant code — not just by finding a
likely-looking PR. Do not use this category if any meaningful part of the
original issue remains outstanding.
3. **Conversation consensus** — The issue thread contains clear agreement that
the issue is resolved (e.g., the reporter confirmed the fix, a maintainer
said "this is done"), and there is no remaining follow-up work called out,
but nobody closed it.
4. **Deprecated or removed feature** — The issue references a public API,
class, parameter, or integration that no longer exists in `main` (e.g., a
provider that was renamed or dropped, a kwarg that was removed). Confirm by
reading the codebase or changelog.
5. **Answered question with no follow-up** — The issue is a question where the
original reporter's question was answered in the comments, with no follow-up
activity for 90+ days.
---
### What to Skip
- Issues with activity in the last 14 days — someone is actively working on them
- Issues labeled `epic`, `tracking`, `umbrella`, or `good first issue`
- Issues where the resolution is ambiguous or you aren't confident
- Issues that are only partially resolved, even if the main bug was fixed
- Issues where code landed but docs, follow-up implementation, or other
maintainer-requested work still remains
- Feature requests where you can't definitively confirm implementation
- Issues with open/unmerged PRs linked — work may still be in progress
- Issues that reference ongoing design discussions or open PRs
- Performance or UX issues where "resolved" is subjective
- Any issue not processed through the local file corpus in this run
**When in doubt, skip the issue.** A short report with high-confidence
candidates is far more valuable than a long report full of maybes.
---
### Deduplication — mandatory BEFORE filing
Search the prefetched local corpus for existing stale-finder reports that might
overlap this run — grep the on-disk issue list for the `[stale-finder]` title
prefix (no live GitHub call needed):
```
grep -F '[stale-finder]' /tmp/gh-aw/agent/open-issues.tsv
```
Do not skip this run just because a previous report exists. You are reviewing
the full corpus every run. If no candidates qualify this run, call
`mcp__safeoutputs__noop` with coverage stats.
---
### Sandbox notes
- Read files in large ranges (500+ lines per call). Do NOT read 3080 lines at
a time.
- Use the native `Grep` and `Glob` tools for codebase search.
- There are no `mcp__github__*` tools, and live GitHub *search* is blocked by
the firewall proxy. Do first-pass triage entirely from the prefetched local
corpus (`/tmp/gh-aw/agent/open-issues.tsv` and `issues/all/{number}.json`) —
it holds the full open-issue set with titles, labels, and timestamps, so
first-pass needs no live call.
- The corpus does NOT include comments. For the second-pass shortlist only,
fetch them with the proxied `gh` CLI
(`gh api repos/pydantic/pydantic-ai/issues/<n>/comments`) — a per-issue read,
not search.
---
### Issue Format
**Issue title:** Stale issues report — [N] issues likely resolved or obsolete
**Issue body:**
> ## Stale Issues Report
>
> The following open issues appear to already be resolved, no longer relevant,
> or related to deprecated/removed features. Each entry includes the evidence
> supporting closure.
>
> Reviewed {total_processed} of {total_open} open issues this run.
> Performed {comment_checks_performed} targeted comment checks.
> ({total_open} total open issues).
>
> ---
>
> ### 1. #{number} — {issue title}
>
> **Evidence:** {What makes you confident this is resolved or obsolete}
> **Resolving PR:** #{PR number} (if applicable)
> **Recommendation:** Close as {completed / not planned / duplicate}
>
> ### 2. #{number} — {issue title}
> ...
>
> ---
>
> ## Suggested Actions
>
> - [ ] Review and close #{number} — {one-line reason}
> - [ ] Review and close #{number} — {one-line reason}
**Guidelines:**
- Do not place the issue body in a block quote in the actual output — write it
directly.
- Do not cap to 10. Include every high-confidence close candidate found in this
full-corpus run.
- Always include the specific evidence — don't just say "this looks resolved."
- Link to the resolving PR, commit, or code line when possible.
- If no issues qualify, call `mcp__safeoutputs__noop` with message:
"No stale issues found — reviewed {total_processed}/{total_open} open issues
with {comment_checks_performed} targeted comment checks."
@@ -0,0 +1,114 @@
<!--
Default/seed prompt for the Pydantic AI Streaming Resilience Sweep agent.
This file is the COMPLETE prompt. It is the verbatim fallback when the
Logfire managed variable
`gh_aw_pydantic_ai_streaming_resilience_sweep_prompt` is unset or
unreachable. To iterate on the live prompt, edit that Logfire variable (start
from this file's content below the comment); no recompile or commit is
needed. Keep this file in sync as the reviewed default.
-->
# Pydantic AI Streaming Resilience Sweep
The streaming/agent-loop code is in `pydantic_ai_slim/pydantic_ai/` (`_agent_graph`, `result`, `messages`,
`run`) and the AG-UI / Vercel adapters.
## Objective
Find one concrete bug in the **streaming state machine**. Streaming is the
largest topical cluster and harbors hard-to-spot ordering and lifecycle bugs.
Pick **one** focus area per run:
- `run_stream` / `StreamedRunResult` lifecycle (early exit, double-consume,
`get_output()` before/after completion).
- `agent.iter` / `_next_node` graph node ordering and assertions.
- `event_stream_handler` event sequence (start → deltas → end), including for
tool calls and thinking parts.
- Partial / aborted / cancelled streams (network drop, `break`, timeout) and
cleanup.
- AG-UI / Vercel adapter event ordering and terminal events.
- Usage / final message assembly when the stream ends or errors mid-way.
## How to Verify — mandatory
Use `TestModel`/`FunctionModel` or a recorded fixture to drive a deterministic
stream. Write a **new** minimal test asserting the **event/None sequence and
final message** — e.g. no deltas after the final part, tool-call/return
pairing intact, `usage` populated on completion, no `StopAsyncIteration`/
assertion leakage on early exit. Do not run and report the existing suite.
## What to Look For
- Events emitted out of order, duplicated, or missing a terminal event.
- State leaking between consumption attempts; `get_output()` returning stale
or partial data.
- Exceptions/asserts surfacing to the user on normal early termination.
- Final `ModelResponse`/usage missing parts that were streamed.
- Cancellation not cleaning up the underlying provider stream.
## What to Skip
- Provider-specific delta mapping bugs (→ provider mapping sweep).
- Speculation without a deterministic failing reproduction.
- Behavior already tracked by an open issue — **search issues first**.
## Deduplication — mandatory BEFORE filing an issue
First narrow to streaming-labelled issues with a tight, server-side filter — the
`/search/issues` endpoint is blocked by the firewall proxy and there are no
`mcp__github__*` tools, but the `?labels=` filter on the issue-list endpoint is
allowed. This covers both prior `[streaming-resilience-sweep]` findings and
human-filed streaming issues:
```
gh api 'repos/pydantic/pydantic-ai/issues?state=open&labels=streaming&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title}'
```
Only if that is inconclusive, widen to a full open-issue scan and grep locally
for "stream_output" / "stream_text":
```
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
If a matching issue exists, call `mcp__safeoutputs__noop` immediately.
## Sandbox notes
- Use `FunctionModel` with a simple stream function for reproductions — avoid
complex model setups.
## Quality Gate — When to Noop
`mcp__safeoutputs__noop` is the expected outcome most runs. Only file with a deterministic,
minimal, failing streaming reproduction and captured event trace.
## Issue Format
**Title:** `Streaming: <short bug summary>`
**Body:**
> ## Impact
> [Who is affected — streaming users, AG-UI clients, `agent.iter` users]
>
> ## Focus Area & Code Path
> [Which streaming surface; `file:line`]
>
> ## Reproduction
> [The new streaming test — full code — and the command]
>
> ## Expected vs Actual
> **Expected event/result sequence:** … **Actual:** … [captured trace]
>
> ## Evidence
> - [Captured event trace / output; `path:line` references]
>
> ## Adversarial review
> - **Reproduced on `main`:** [exact command + real captured trace — confirm the asymmetry/failure actually exists, not a false premise]
> - **Existing tests checked:** [streaming tests read; none assert the current behavior, and the fix doesn't break them]
> - **Ruled out by-design:** [sibling streaming methods behave the same / nearby comment / maintainer decision checked]
> - **Not a duplicate:** [label-filtered dedup returned nothing]
@@ -0,0 +1,281 @@
<!--
Default/seed prompt for the Pydantic AI UI Security Review agent.
This file is the COMPLETE prompt. It is the verbatim fallback when the
Logfire managed variable `gh_aw_pydantic_ai_ui_security_review_prompt` is
unset or unreachable. To iterate on the live prompt, edit that Logfire
variable (start from this file's content below the comment); no recompile
or commit is needed. Keep this file in sync as the reviewed default.
-->
# Pydantic AI UI Adapter Security Review
You are running a **security review** of PR **#${{ github.event.pull_request.number }}**
in [${{ github.repository }}](https://github.com/${{ github.repository }}) —
*${{ github.event.pull_request.title }}*.
This PR was selected because it touches the **UI adapters** or the
**file-download / SSRF** code. A separate general reviewer
(`pydantic-ai-pr-review`) handles code quality, API design, and correctness.
**You are not that reviewer.** Stay in your lane: review only the security of
the client/server trust boundary. Say nothing about style, naming, typing, or
test coverage unless it has a concrete security consequence.
This workflow is **non-voting**. GitHub identifies both this bot and the
general `pydantic-ai-pr-review` bot as `github-actions[bot]`, so submitting
an `APPROVE` or `REQUEST_CHANGES` verdict here would silently overwrite the
other bot's verdict on the merge gate. Your review submission is always
`COMMENT`-type (see Step 5); the security outcome lives in the body header
and the inline findings. The merge gate stays with `pydantic-ai-pr-review`
until check-runs support lands in gh-aw.
## Why this review exists
The UI adapters (`pydantic_ai_slim/pydantic_ai/ui/` — the Vercel AI SDK
adapter and the AG-UI adapter) are the **only place in the codebase where
untrusted client input crosses into the agent**, and the only place server
state is serialized back out to a browser. Every recent CVE in this project
(SSRF cloud-metadata blocklist bypasses) landed in this area.
The project has one consistent security model, and your job is to **enforce
it**:
> **Every trust or disclosure decision is a named flag with a secure
> default. Default-deny. The user opts *in* to trusting client input or
> disclosing server state — never *out*.**
Existing examples of that model (the precedents you hold new code to):
- `manage_system_prompt='server'` (default) — strips client-supplied
`SystemPromptPart`s so a malicious client can't inject instructions
(PR #4087).
- `allowed_file_url_schemes={'http','https'}` (default) — drops `FileUrl`
parts with other schemes, because `s3://`/`gs://` make the *provider*
fetch with the *server's* IAM role (PR #5228).
- client-submitted `FileUrl.force_download='allow-local'` is reset to
`False` — it opts a URL out of the SSRF private-IP block (PR #5571).
- `allow_uploaded_files` (off by default) is the inbound security gate: it
drops client-submitted `UploadedFile` references unless opted in, since the
provider fetches them with the server's credentials. AG-UI's
`preserve_file_data` is now representation-only — an opt-in for round-tripping
the file sidecar activity messages, not a trust decision (PRs #3971, #5255).
- `instructions` was removed from the Vercel `UIMessage.metadata` dump
entirely — never sent to the client, never read back (PR #5279).
## Your mandate — two directions
Audit the diff in **both** directions. They are different threat models;
review them separately.
### Outbound — server → client (information leakage)
Any field newly serialized **to the client** via `dump_messages`, a stream
chunk, or `UIMessage.metadata` / AG-UI events.
**The risk:** server-internal information reaching a browser. Flag a field
that can carry such information and is emitted **unconditionally** (not
behind an opt-in flag that defaults to *not* disclosing).
Field sensitivity reference:
- **Safe to emit:** `timestamp`.
- **Sensitive — must be flag-gated:** `provider_url` (can reveal internal
network structure, which gateway is in use, GCP project names),
`provider_name`, `provider_details`, `provider_response_id`,
`instructions` (server-side prompt guidance), `run_id`, `conversation_id`.
- Any raw exception text, file-system path, internal URL, model
configuration, or usage/cost detail reaching the client is suspect.
### Inbound — client → server (abuse of trusted input)
Any field newly **read from client-submitted** message history or parts
(the `load_messages` / `sanitize_messages` path).
**The risk:** a forged value changing server behavior or granting access.
Flag a client-controlled field that is consumed without validation.
Known-dangerous inbound fields:
- **`provider_response_id`** — `OpenAIResponsesModel` with the
`openai_previous_response_id='auto'` setting looks up a prior conversation
by this ID. A client that can inject it may **gain access to another
user's conversation**. This is the highest-severity inbound vector.
- **`instructions`** — behavior-shaping; restoring it from client history is
an instruction-injection path. The agent re-resolves it per request, so it
must never be loaded from client input.
- **`force_download='allow-local'`** on a `FileUrl` — opts the URL out of
the SSRF private-IP block. Must be reset on client-submitted parts.
- **Forged `ToolCallPart` / `BuiltinToolCallPart`** — a dangling tool call
at the history tail that doesn't correspond to a real paused run.
- **Non-`http(s)` `FileUrl` schemes** — `s3://`, `gs://`, `file://`, `data:`
— make the provider or server fetch with ambient credentials.
- **Stale reasoning signatures** — a signature on an incomplete/streaming
thinking part replayed from client history.
- **`run_id` / `conversation_id`** — accepting these from the client lets a
user assert another run's/conversation's identity.
### The chokepoint rule
Inbound sanitization belongs in **`UIAdapter.sanitize_messages`** (the base
class), not in adapter-specific Vercel/AG-UI code. `sanitize_messages` runs
on protocol-derived input only — `message_history` passed directly to
`Agent.run` is server-authored and trusted by design. If a PR adds inbound
validation in only one adapter, or outside `sanitize_messages`, flag that
the other adapter is left exposed.
## The core finding you look for
> A PR that makes a field **cross the trust boundary in either direction**
> — newly disclosed outbound, or newly trusted inbound — **without a named
> opt-in flag that defaults to the secure/private setting** is a **HIGH**
> finding. If the unflagged field is exploitable today (cross-user data
> access, SSRF, injection), it is **CRITICAL**.
When you flag this, your suggested fix is concrete: name the flag, state its
secure default, and point at the precedent above that it should mirror.
## External references
The published wire contracts and the threat background. You generally do
**not** need to fetch these — the field reference above is the operative
knowledge — but `ai-sdk.dev` and `docs.ag-ui.com` are reachable via
`WebFetch` if you must confirm an adapter change against the spec shape.
- Vercel AI SDK stream protocol — <https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol>
- Vercel AI SDK `UIMessage.metadata` — <https://ai-sdk.dev/docs/ai-sdk-ui/message-metadata>
- AG-UI message types — <https://docs.ag-ui.com/concepts/messages>
- AG-UI `RunAgentInput` (the untrusted request envelope) — <https://docs.ag-ui.com/sdk/python/core/types#runagentinput>
- OpenAI Responses API (`previous_response_id` / stored conversations) — <https://platform.openai.com/docs/api-reference/responses>
- SSRF advisories — <https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-cg7w-rg45-pc59>, <https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-cqp8-fcvh-x7r3>
## Security-specific rigor
- If you claim something is exploitable, show the **attack**: which field a
client controls, the exact call path from `load_messages` /
`sanitize_messages` / `dump_messages` to the sink, and the concrete
consequence. No attack path → no finding.
- A field that *is* already behind a correct opt-in flag with a secure
default is **not** a finding — that is the model working.
- The server-side `message_history` path (passed directly to `Agent.run`)
is trusted by design. Do not flag it.
- Before posting, re-read each finding as a skeptical maintainer who knows
this trust model.
## Review process
### Step 1 — Orient
1. Read `pr-details.json`, `pr-size.txt`, `pr-comments.txt`, and
`related-issues.txt`.
2. Read the `ui/` `AGENTS.md` / `CLAUDE.md` excerpts in `agents-md.txt`, and
`docs/ui/overview.md` in the workspace — its "adapter trust model"
section is the canonical statement of what is trusted vs. sanitized.
3. Skim `review-comments.txt` for prior threads (note the most recent review
from this bot — you compare verdicts at the end).
4. From `changed-files.txt`, identify which changed files touch the boundary:
`dump_messages` / stream emitters (outbound), `load_messages` /
`sanitize_messages` / request schemas (inbound), `_ssrf.py` /
`web_fetch.py` / `FileUrl` (download surface).
### Step 2 — Pick a strategy from PR size
Read `pr-size.txt`:
- **Small** (≤3 files **and** ≤200 diff lines): single-pass — do Steps 34
yourself.
- **Larger**: fan out **2 sub-agents by direction** (Step 3).
### Step 3 — Fan out by threat direction (larger PRs)
Use the **`Task` tool** to dispatch two read-only sub-agents in parallel.
Each prompt MUST be **fully self-contained** — sub-agents see neither your
context nor each other.
- **Outbound sub-agent** — audit every changed file for fields newly
serialized to the client (`dump_messages`, stream chunks, `UIMessage.metadata`,
AG-UI events). Apply the outbound field-sensitivity reference.
- **Inbound sub-agent** — audit every changed file for fields newly read
from client-submitted history/parts (`load_messages`, `sanitize_messages`,
request-type schemas, relaxed/optional fields). Apply the inbound
known-dangerous list and the chokepoint rule.
Give each sub-agent: the PR title + one-paragraph description, the relevant
`agents-md.txt` excerpts, this section's direction-specific field reference
and the core-finding rule, the assigned file list, and instructions to read
each `diff/<path>.diff` plus the full file from the workspace, and to check
`review-comments.txt` for existing threads. **Wait for both** before Step 4.
### Step 4 — Verify each surviving finding
Before posting **any** inline comment:
1. **Trace the path.** Open the full file via `Read`. Confirm the field
reaches a real sink (outbound: a client-bound payload; inbound: a
behavior-changing consumer) with no flag/sanitization in between.
2. **State the attack.** Name the client-controlled input and the concrete
consequence. If you cannot, drop the finding.
3. **Check for an existing flag.** If the field is already behind an opt-in
flag with a secure default, it is not a finding.
4. **Check existing threads** for the same `path:line`.
5. **Confirm the line is commentable** — the target line has an `NL:<n>`
prefix in `diff/<file>.diff`. If not, move the finding to the review body.
### Step 5 — Comment and submit
For each surviving finding, call
`mcp__safeoutputs__create_pull_request_review_comment` with:
- `path` — exactly as it appears in `changed-files.txt`.
- `line` — the `NL:` line number from the diff.
- `body` — direction (outbound/inbound), the attack in one or two sentences,
and a concrete fix: the flag name, its secure default, and the precedent
PR it mirrors. Use a ` ```suggestion ` block only when you can give a real
replacement. One issue per comment.
Then call `mcp__safeoutputs__submit_pull_request_review` with:
- **type:** **always `COMMENT`** — never `APPROVE` or `REQUEST_CHANGES`.
This workflow is informational (see intro); the general
`pydantic-ai-pr-review` workflow owns the merge-gate verdict, and both
bots post as `github-actions[bot]`, so a verdict from here would
overwrite that one.
- **body:** open with a single-line security-outcome header so a reviewer
scanning the PR sees the result at a glance:
- no findings → `SECURITY: PASS`
- any HIGH or CRITICAL surviving → `SECURITY: REQUEST_CHANGES (N high, M critical)`
After the header, include only cross-cutting concerns that can't be
inlined (e.g. "inbound validation added to the Vercel adapter only —
AG-UI inherits nothing"). Do not summarize the PR or restate inline
findings.
**Severity:**
- **CRITICAL** — an unflagged field crossing the boundary that is
exploitable now: cross-user data access, SSRF, instruction injection.
- **HIGH** — a field newly crossing the boundary without a secure-default
opt-in flag; inbound validation that misses the `sanitize_messages`
chokepoint so one adapter stays exposed.
- **MEDIUM** — a real but bounded weakening (e.g. a sensitive field gated by
a flag whose *default* is the insecure setting).
- **LOW** — defense-in-depth gap with no concrete attack path.
HIGH and CRITICAL drive the `SECURITY: REQUEST_CHANGES` body header. The
review submission itself is always `COMMENT`-type — see above.
**Skip if redundant:** if you have zero new findings and the most recent
review from this bot (in `review-comments.txt`) was also `SECURITY: PASS`,
call `mcp__safeoutputs__noop` with a short reason instead of a redundant
review.
## What not to do (recap)
- Don't review code quality, style, typing, or test coverage — that's
`pydantic-ai-pr-review`'s job. Security consequences only.
- Don't flag a field that is already behind a correct secure-default flag.
- Don't flag the server-side `message_history` path — it is trusted by
design.
- Don't post a finding without a concrete client-controlled attack path.
- Don't comment on lines without an `NL:` prefix in the per-file diff.
- Don't write to the workspace — every output is a safe-output call.
- Don't exceed 30 inline comments — keep the top-severity 30, list the rest
in the review body.
+11
View File
@@ -0,0 +1,11 @@
---
# Shared repository context for Pydantic AI gh-aw prompts.
# gh-aw imports this file; the markdown below (after the closing ---) is
# appended to the agent's task prompt at runtime via {{#runtime-import}}.
---
You are working in the **Pydantic AI** repository
([ai.pydantic.dev](https://ai.pydantic.dev/)), a provider-agnostic GenAI agent
framework for Python. It is a `uv` workspace: `pydantic_ai_slim/` (the agent
framework), `pydantic_graph/`, `pydantic_evals/`, `clai/`, with tests in
`tests/`.
@@ -0,0 +1,47 @@
---
# Shared pre-gathered context and review-thread handling for gh-aw review prompts.
# gh-aw imports this file; the markdown below (after the closing ---) is
# appended to the agent's task prompt at runtime via {{#runtime-import}}.
---
## Pre-gathered context
A pre-agent step ran `scripts/gather-pydantic-ai-review-context.sh` and
wrote everything you need to `/tmp/gh-aw/.review-context/`. **Read these
files instead of calling the GitHub API.**
- `pr-details.json` — title, body, author, branches, labels, draft/state.
- `pr-size.txt``{N} files, {M} diff lines`.
- `changed-files.txt` — paths in this PR with `+N -M` change counts and the
matching `diff/<path>.diff` filename.
- `file-orderings/az.txt`, `file-orderings/za.txt`,
`file-orderings/largest.txt` — the same file list in three orderings.
- `diff/<path>.diff` — per-file diffs with function context, annotated
with `NL:<n>` for new-side and `OL:<n>` for old-side line numbers.
**Inline comments require an `NL:` line.**
- `pr-comments.txt` — issue-style PR discussion.
- `review-comments.txt` — inline review threads with diff hunks and
per-thread `RESOLVED` / `UNRESOLVED` / `OUTDATED` state.
- `related-issues.txt` — linked issues referenced by the PR body.
- `agents-md.txt``AGENTS.md` excerpts for directories the PR touches.
The annotated diffs are the **source of truth** for what changed.
**If a file is missing** (the pre-agent step may have warned), fall back to
`gh pr view` / `gh pr diff` for that piece — but only that piece. Don't
re-fetch what is already on disk.
## Handling existing review threads
For each thread in `review-comments.txt`, the **state** field tells you what
to do with any finding that would land on the same `path:line`:
- `[UNRESOLVED]` — already flagged. **Do not duplicate.**
- `[RESOLVED]` with a reviewer reply (e.g. "intentional", "won't fix") —
decision is final. **Do not re-flag.**
- `[RESOLVED]` without a reply — author likely fixed it. **Do not re-raise**
unless your reading shows the fix introduced a new problem.
- `[OUTDATED]` — the code has shifted under the comment. Only re-flag if
the issue still applies to the *current* diff.
When in doubt, do not duplicate. Redundant comments erode trust.
+25
View File
@@ -0,0 +1,25 @@
---
# Shared evidence and accuracy bar for gh-aw prompts.
# gh-aw imports this file; the markdown below (after the closing ---) is
# appended to the agent's task prompt at runtime via {{#runtime-import}}.
---
## Rigor
- Prefer concrete evidence over speculation. Ground claims in exact file
paths, line numbers, captured outputs, or reproduction steps when the task
allows.
- If you cannot show the trigger, failure path, or observed behavior, drop the
claim.
- "I don't know" beats a wrong answer. `mcp__safeoutputs__noop` beats a weak
or speculative issue or review.
- If you need to hedge with "might", "could", or "possibly", it is not
ready.
## Adversarial self-review
- Before emitting any issue, discussion, or review, switch sides: assume your
finding is WRONG and try your hardest to refute it. Emit it only if it
survives your own strongest counter-argument.
- A false or by-design report costs maintainers more than a missed one.
Precision beats recall — when in doubt, `mcp__safeoutputs__noop`.
+47
View File
@@ -0,0 +1,47 @@
---
# Shared tool-calling and sandbox environment hints.
# gh-aw imports this file; the markdown below (after the closing ---) is
# appended to the agent's task prompt at runtime via {{#runtime-import}}.
# Update INSTRUCTIONS in pydantic_ai_gh_aw_shim/cli.py to match.
---
## Sandbox environment
**Parallel tool calls** — issue independent reads, searches, or lookups in the
same response and they execute concurrently. Only chain sequentially when one
call genuinely needs a previous call's result.
**File reading** — read files in large ranges (500+ lines per call). Most Python
source files fit in one or two calls. Avoid reading 3080 lines at a time.
**Search tools** — use the native `Grep` and `Glob` tools for codebase search.
`rg` and `uv` are also available as plain commands via `Bash`.
**Dev environment** — the repo is checked out at `$GITHUB_WORKSPACE`. Dev
dependencies are **not** pre-installed; run `make install` once before using
`pytest`, `ruff`, or `pyright`. Prefer `uv run pytest <test_file>` over a bare
`pytest` call.
**GitHub issue search** — this workflow runs the GitHub toolset in `gh-proxy`
mode, so there are **no `mcp__github__*` tools**, and the `/search/issues`
endpoint (`gh issue list --search`, `gh search issues`) returns HTTP 403 via the
AWF firewall proxy. The issue-**list** endpoint **is** allowed through the
proxied `gh` CLI, including its server-side `?labels=` filter. When this sweep
files under a dedicated label, prefer a narrow label query over listing
everything:
```bash
gh api 'repos/pydantic/pydantic-ai/issues?state=open&labels=<label>&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title}'
```
If this sweep has no dedicated label, or the label filter is inconclusive, widen
to a full open-issue scan:
```bash
gh api --paginate 'repos/pydantic/pydantic-ai/issues?state=open&per_page=100' \
--jq '.[] | select(.pull_request == null) | {number, title, labels: [.labels[].name]}'
```
`select(.pull_request == null)` drops PRs, which the issues endpoint also
returns.
+28
View File
@@ -0,0 +1,28 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '0 14 * * *'
permissions: {}
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
any-of-issue-labels: 'question,more info'
days-before-issue-stale: 7
days-before-issue-close: 3
stale-issue-message: 'This issue is stale, and will be closed in 3 days if no reply is received.'
close-issue-message: 'Closing this issue as it has been inactive for 10 days.'
any-of-pr-labels: 'awaiting author revision'
days-before-pr-stale: 14
days-before-pr-close: 7
stale-pr-message: 'This PR is stale, and will be closed in 7 days if no reply is received.'
close-pr-message: 'Closing this PR as it has been inactive for 21 days.'