diff --git a/.github/workflows/on_issues_ai.yml b/.github/workflows/on_issues_ai.yml
new file mode 100644
index 0000000000..f47d051e65
--- /dev/null
+++ b/.github/workflows/on_issues_ai.yml
@@ -0,0 +1,654 @@
+# yamllint disable rule:line-length
+---
+name: AI issue triage and draft fix
+
+on:
+ issues:
+ types: [opened, reopened, labeled]
+
+concurrency:
+ group: ai-issue-${{ github.event.issue.number }}
+ cancel-in-progress: false
+
+env:
+ TRIAGE_MODEL: ${{ vars.OPENAI_TRIAGE_MODEL || 'gpt-5-mini' }}
+ MIN_CONFIDENCE: ${{ vars.AI_MIN_CONFIDENCE || '0.80' }}
+
+jobs:
+ detect_submitter:
+ if: >-
+ github.event.action != 'labeled' ||
+ github.event.label.name == 'ai: fix-approved'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ matched: ${{ steps.submitter.outputs.matched }}
+ addon: ${{ steps.submitter.outputs.addon }}
+ submitter: ${{ steps.submitter.outputs.submitter }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Detect mapped add-on submitter
+ id: submitter
+ env:
+ ISSUE_TITLE: ${{ github.event.issue.title }}
+ ISSUE_BODY: ${{ github.event.issue.body }}
+ run: bash .github/scripts/find_addon_submitter.sh
+
+ triage:
+ if: >-
+ needs.detect_submitter.outputs.matched != 'true' &&
+ (
+ github.event.action == 'opened' ||
+ github.event.action == 'reopened' ||
+ (
+ github.event.action == 'labeled' &&
+ github.event.label.name == 'ai: fix-approved'
+ )
+ )
+ needs: detect_submitter
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ outputs:
+ addon: ${{ steps.result.outputs.addon }}
+ category: ${{ steps.result.outputs.category }}
+ confidence: ${{ steps.result.outputs.confidence }}
+ existing_addon: ${{ steps.result.outputs.existing_addon }}
+ risk: ${{ steps.result.outputs.risk }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Ensure AI labels exist
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ gh label create "ai: triaged" --repo "$REPO" --color "1d76db" --description "Issue classified by AI" --force
+ gh label create "ai: answered" --repo "$REPO" --color "0e8a16" --description "Question answered automatically" --force
+ gh label create "ai: needs-info" --repo "$REPO" --color "fbca04" --description "AI requested essential information" --force
+ gh label create "ai: fix-proposed" --repo "$REPO" --color "c5def5" --description "AI recommends repository analysis" --force
+ gh label create "ai: fix-approved" --repo "$REPO" --color "5319e7" --description "Maintainer approved an automated fix attempt" --force
+ gh label create "ai: fixing" --repo "$REPO" --color "0052cc" --description "Automated fix attempt is running" --force
+ gh label create "ai: pr-created" --repo "$REPO" --color "0e8a16" --description "Automated draft pull request created" --force
+ gh label create "ai: maintainer-review" --repo "$REPO" --color "d93f0b" --description "Maintainer review is required" --force
+ gh label create "ai: new-addon-request" --repo "$REPO" --color "ededed" --description "New add-on request; never implemented automatically" --force
+ gh label create "ai-generated" --repo "$REPO" --color "bfdadc" --description "Changes generated with AI assistance" --force
+
+ - name: Triage issue with structured output
+ id: result
+ env:
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
+ ISSUE_BODY: ${{ github.event.issue.body }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ ISSUE_TITLE: ${{ github.event.issue.title }}
+ run: |
+ set -euo pipefail
+ test -n "$OPENAI_API_KEY"
+
+ issue_body="${ISSUE_BODY:0:20000}"
+ addon_catalog="$(
+ find . -mindepth 2 -maxdepth 2 -type f \
+ \( -name config.yaml -o -name config.json \) -printf '%h\n' |
+ sed 's#^./##' | sort -u |
+ jq -Rsc 'split("\n") | map(select(length > 0))'
+ )"
+
+ jq -n \
+ --arg model "$TRIAGE_MODEL" \
+ --arg instructions "$(cat .github/ai/triage-prompt.md)" \
+ --arg author "$ISSUE_AUTHOR" \
+ --arg body "$issue_body" \
+ --arg number "$ISSUE_NUMBER" \
+ --arg title "$ISSUE_TITLE" \
+ --argjson addon_catalog "$addon_catalog" \
+ --slurpfile schema .github/ai/triage-schema.json \
+ '{
+ model: $model,
+ store: false,
+ max_output_tokens: 1200,
+ reasoning: {effort: "low"},
+ instructions: $instructions,
+ input: (
+ "Repository issue #" + $number + "\n" +
+ "Author: " + $author + "\n" +
+ "Title: " + $title + "\n\n" +
+ "Existing add-on directories:\n" +
+ ($addon_catalog | tojson) + "\n\n" +
+ "Body:\n" + $body
+ ),
+ text: {
+ format: {
+ type: "json_schema",
+ name: "issue_triage",
+ strict: true,
+ schema: $schema[0]
+ }
+ }
+ }' > "$RUNNER_TEMP/openai-request.json"
+
+ curl --fail-with-body --retry 3 --max-time 120 \
+ -H "Authorization: Bearer $OPENAI_API_KEY" \
+ -H "Content-Type: application/json" \
+ https://api.openai.com/v1/responses \
+ --data-binary "@$RUNNER_TEMP/openai-request.json" \
+ > "$RUNNER_TEMP/openai-response.json"
+
+ jq -r '
+ [
+ .output[]? |
+ select(.type == "message") |
+ .content[]? |
+ select(.type == "output_text") |
+ .text
+ ] | join("")
+ ' "$RUNNER_TEMP/openai-response.json" > "$RUNNER_TEMP/triage.json"
+
+ jq -e '
+ (.category | type == "string") and
+ (.confidence | type == "number") and
+ (.risk | type == "string") and
+ (.summary | type == "string") and
+ (.response | type == "string") and
+ (.missing_information | type == "array")
+ ' "$RUNNER_TEMP/triage.json" > /dev/null
+
+ category="$(jq -r '.category' "$RUNNER_TEMP/triage.json")"
+ addon="$(jq -r '.addon // ""' "$RUNNER_TEMP/triage.json")"
+ existing_addon=false
+ if [[ -n "$addon" && "$addon" != */* && "$addon" != "." && "$addon" != ".." ]] &&
+ [[ -f "$addon/config.yaml" || -f "$addon/config.json" ]]; then
+ existing_addon=true
+ fi
+
+ echo "addon=$addon" >> "$GITHUB_OUTPUT"
+ echo "category=$category" >> "$GITHUB_OUTPUT"
+ echo "confidence=$(jq -r '.confidence' "$RUNNER_TEMP/triage.json")" >> "$GITHUB_OUTPUT"
+ echo "existing_addon=$existing_addon" >> "$GITHUB_OUTPUT"
+ echo "risk=$(jq -r '.risk' "$RUNNER_TEMP/triage.json")" >> "$GITHUB_OUTPUT"
+
+ - name: Publish triage result
+ if: github.event.action == 'opened' || github.event.action == 'reopened'
+ env:
+ EXISTING_ADDON: ${{ steps.result.outputs.existing_addon }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ISSUE_ACTION: ${{ github.event.action }}
+ ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ ISSUE_UPDATED_AT: ${{ github.event.issue.updated_at }}
+ REPO: ${{ github.repository }}
+ REPOSITORY_OWNER: ${{ github.repository_owner }}
+ run: |
+ set -euo pipefail
+
+ marker=""
+ existing="$(
+ gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json comments |
+ jq --arg marker "$marker" \
+ '[.comments[] | select(.body | contains($marker))] | length'
+ )"
+ [[ "$existing" -eq 0 ]] || exit 0
+
+ category="$(jq -r '.category' "$RUNNER_TEMP/triage.json")"
+ confidence="$(jq -r '.confidence' "$RUNNER_TEMP/triage.json")"
+ risk="$(jq -r '.risk' "$RUNNER_TEMP/triage.json")"
+ summary="$(jq -r '.summary' "$RUNNER_TEMP/triage.json")"
+ response="$(jq -r '.response' "$RUNNER_TEMP/triage.json")"
+
+ confident=false
+ if awk -v confidence="$confidence" -v minimum="$MIN_CONFIDENCE" \
+ 'BEGIN { exit !(confidence >= minimum) }'; then
+ confident=true
+ fi
+
+ labels=("ai: triaged")
+ heading="### AI triage"
+ body="$response"
+
+ if [[ "$confident" != true ]]; then
+ labels+=("ai: maintainer-review")
+ body="**Low-confidence assessment (${confidence}):** ${summary}
+
+ A maintainer should review this issue before any automated action."
+ else
+ case "$category" in
+ question)
+ if [[ "$(jq -r '.safe_to_answer_automatically' "$RUNNER_TEMP/triage.json")" == true ]]; then
+ labels+=("ai: answered")
+ else
+ labels+=("ai: maintainer-review")
+ fi
+ ;;
+ missing_information)
+ heading="### Additional information required"
+ labels+=("ai: needs-info")
+ missing="$(
+ jq -r '
+ if (.missing_information | length) == 0 then
+ ""
+ else
+ "\n\n**Needed:**\n" +
+ (.missing_information | map("- " + .) | join("\n"))
+ end
+ ' "$RUNNER_TEMP/triage.json"
+ )"
+ body="${response}${missing}"
+ ;;
+ bug)
+ labels+=("bug" "ai: fix-proposed")
+ body="**Assessment:** ${summary}
+
+ **Estimated risk:** ${risk}
+
+ ${response}"
+ if [[ "$ISSUE_AUTHOR" == "$REPOSITORY_OWNER" ]]; then
+ labels+=("ai: fixing")
+ body="${body}
+
+ Repository analysis and a draft fix attempt will start automatically because the issue was opened by the repository owner."
+ else
+ body="${body}
+
+ A maintainer can approve repository analysis and an automated draft fix attempt by adding the \`ai: fix-approved\` label."
+ fi
+ ;;
+ improvement)
+ labels+=("enhancement")
+ if [[ "$EXISTING_ADDON" == true ]]; then
+ labels+=("ai: fix-proposed" "ai: fixing")
+ body="**Assessment:** ${summary}
+
+ **Estimated risk:** ${risk}
+
+ ${response}
+
+ This targets an existing add-on, so repository analysis and a validated draft fix attempt will start automatically."
+ else
+ labels+=("ai: maintainer-review")
+ body="${response}
+
+ The referenced add-on directory could not be verified, so no automated implementation will start."
+ fi
+ ;;
+ new_addon_request)
+ labels+=("enhancement" "ai: new-addon-request" "ai: maintainer-review")
+ body="${response}
+
+ New add-on requests are never accepted or implemented automatically by this workflow. A maintainer must review the proposal manually."
+ ;;
+ unsupported | spam)
+ labels+=("ai: maintainer-review")
+ ;;
+ esac
+ fi
+
+ body="${marker}
+ ${heading}
+
+ ${body}
+
+ Automated classification using \`${TRIAGE_MODEL}\`; confidence ${confidence}."
+
+ for label in "${labels[@]}"; do
+ if ! gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$label"; then
+ if [[ "$label" == "bug" || "$label" == "enhancement" ]]; then
+ echo "Optional repository label '$label' does not exist; continuing." >&2
+ else
+ exit 1
+ fi
+ fi
+ done
+ gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"
+
+ codex_fix:
+ if: >-
+ always() &&
+ needs.detect_submitter.result == 'success' &&
+ needs.detect_submitter.outputs.matched != 'true' &&
+ needs.triage.result == 'success' &&
+ fromJSON(needs.triage.outputs.confidence) >=
+ fromJSON(vars.AI_MIN_CONFIDENCE || '0.80') &&
+ (
+ (
+ needs.triage.outputs.category == 'improvement' &&
+ needs.triage.outputs.existing_addon == 'true'
+ ) ||
+ (
+ needs.triage.outputs.category == 'bug' &&
+ (
+ (
+ github.event.action == 'labeled' &&
+ github.event.label.name == 'ai: fix-approved'
+ ) ||
+ (
+ (github.event.action == 'opened' || github.event.action == 'reopened') &&
+ github.event.issue.user.login == github.repository_owner
+ )
+ )
+ )
+ )
+ needs: [detect_submitter, triage]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+ outputs:
+ addon: ${{ steps.prepare.outputs.addon }}
+ branch: ${{ steps.prepare.outputs.branch }}
+ category: ${{ steps.prepare.outputs.category }}
+ existing_pr: ${{ steps.prepare.outputs.existing_pr }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Check for an existing pull request
+ id: prepare
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ REPO: ${{ github.repository }}
+ TRIAGE_ADDON: ${{ needs.triage.outputs.addon }}
+ TRIAGE_CATEGORY: ${{ needs.triage.outputs.category }}
+ run: |
+ set -euo pipefail
+ branch="ai/issue-${ISSUE_NUMBER}"
+ existing_pr="$(
+ gh pr list --repo "$REPO" --state open --head "$branch" \
+ --json url --jq '.[0].url // ""'
+ )"
+ echo "addon=$TRIAGE_ADDON" >> "$GITHUB_OUTPUT"
+ echo "branch=$branch" >> "$GITHUB_OUTPUT"
+ echo "category=$TRIAGE_CATEGORY" >> "$GITHUB_OUTPUT"
+ echo "existing_pr=$existing_pr" >> "$GITHUB_OUTPUT"
+
+ - name: Build isolated issue context
+ if: steps.prepare.outputs.existing_pr == ''
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ REPO: ${{ github.repository }}
+ TRIAGE_ADDON: ${{ needs.triage.outputs.addon }}
+ TRIAGE_CATEGORY: ${{ needs.triage.outputs.category }}
+ TRIAGE_RISK: ${{ needs.triage.outputs.risk }}
+ run: |
+ set -euo pipefail
+ gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
+ --json number,title,body,author,comments,labels,url |
+ jq \
+ --arg addon "$TRIAGE_ADDON" \
+ --arg category "$TRIAGE_CATEGORY" \
+ --arg risk "$TRIAGE_RISK" \
+ '. + {automation_triage: {addon: $addon, category: $category, risk: $risk}}' \
+ > ai-issue-context.json
+ cp .github/ai/fix-prompt.md codex-prompt.md
+
+ - name: Run Codex
+ id: codex
+ if: steps.prepare.outputs.existing_pr == ''
+ uses: openai/codex-action@v1
+ with:
+ openai-api-key: ${{ secrets.OPENAI_API_KEY }}
+ prompt-file: codex-prompt.md
+ output-file: codex-result.md
+ sandbox: workspace-write
+ safety-strategy: drop-sudo
+ allow-users: "*"
+ model: ${{ vars.OPENAI_FIX_MODEL }}
+ effort: high
+
+ - name: Package proposed patch
+ if: steps.prepare.outputs.existing_pr == ''
+ run: |
+ set -euo pipefail
+ test -f codex-result.md
+ cp codex-result.md "$RUNNER_TEMP/codex-result.md"
+ rm -f ai-issue-context.json codex-prompt.md codex-result.md
+ git add -A
+ git diff --cached --binary --full-index > "$RUNNER_TEMP/ai.patch"
+ git reset
+ cp "$RUNNER_TEMP/ai.patch" ai.patch
+ cp "$RUNNER_TEMP/codex-result.md" codex-result.md
+
+ - name: Upload proposed patch
+ if: steps.prepare.outputs.existing_pr == ''
+ uses: actions/upload-artifact@v4
+ with:
+ name: ai-issue-${{ github.event.issue.number }}-${{ github.run_id }}
+ path: |
+ ai.patch
+ codex-result.md
+ if-no-files-found: error
+ retention-days: 3
+
+ report_codex_failure:
+ if: always() && needs.codex_fix.result == 'failure'
+ needs: codex_fix
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - name: Report failed automated analysis
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ REPO: ${{ github.repository }}
+ RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ run: |
+ set -euo pipefail
+ for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --remove-label "$label" || true
+ done
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --add-label "ai: maintainer-review"
+ gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body \
+ "### Automated analysis failed
+
+ The automated repository analysis did not complete, so no pull request was created. A maintainer should review the issue and the [workflow run](${RUN_URL})."
+
+ publish_fix:
+ if: >-
+ always() &&
+ needs.codex_fix.result == 'success' &&
+ needs.codex_fix.outputs.existing_pr == ''
+ needs: codex_fix
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Download proposed patch
+ uses: actions/download-artifact@v4
+ with:
+ name: ai-issue-${{ github.event.issue.number }}-${{ github.run_id }}
+
+ - name: Apply patch
+ run: |
+ set -euo pipefail
+ if [[ -s ai.patch ]]; then
+ git apply --index --3way ai.patch
+ fi
+ rm -f ai.patch codex-result.md
+
+ - name: Restore Codex report
+ uses: actions/download-artifact@v4
+ with:
+ name: ai-issue-${{ github.event.issue.number }}-${{ github.run_id }}
+ path: ${{ runner.temp }}/ai-result
+
+ - name: Ensure publication labels exist
+ env:
+ GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ gh label create "ai: maintainer-review" --repo "$REPO" --color "d93f0b" --description "Maintainer review is required" --force
+ gh label create "ai: pr-created" --repo "$REPO" --color "0e8a16" --description "Automated draft pull request created" --force
+ gh label create "ai-generated" --repo "$REPO" --color "bfdadc" --description "Changes generated with AI assistance" --force
+
+ - name: Validate proposed patch
+ id: validation
+ env:
+ AI_EXPECTED_ADDON: ${{ needs.codex_fix.outputs.addon }}
+ AI_MAX_CHANGED_FILES: ${{ vars.AI_MAX_CHANGED_FILES || '25' }}
+ AI_MAX_CHANGED_LINES: ${{ vars.AI_MAX_CHANGED_LINES || '2000' }}
+ AI_REQUEST_CATEGORY: ${{ needs.codex_fix.outputs.category }}
+ run: |
+ set -o pipefail
+ bash .github/scripts/validate_ai_patch.sh origin/master 2>&1 |
+ tee "$RUNNER_TEMP/ai-validation.log"
+
+ - name: Report validation failure
+ if: failure() && steps.validation.outcome == 'failure'
+ env:
+ GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ REPO: ${{ github.repository }}
+ RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ run: |
+ set -euo pipefail
+ report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md" 2>/dev/null || true)"
+ validation="$(tail -n 80 "$RUNNER_TEMP/ai-validation.log" 2>/dev/null || true)"
+ body="### Automated fix blocked by validation
+
+ ${report}
+
+
+ Validation output
+
+ \`\`\`text
+ ${validation}
+ \`\`\`
+
+
+ [Open the workflow run](${RUN_URL})"
+ for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --remove-label "$label" || true
+ done
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --add-label "ai: maintainer-review"
+ gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"
+
+ - name: Report no safe change
+ if: steps.validation.outputs.has_changes == 'false'
+ env:
+ GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md")"
+ body="### Automated repository analysis
+
+ ${report}
+
+ No draft pull request was created because Codex produced no repository change."
+ for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --remove-label "$label" || true
+ done
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --add-label "ai: maintainer-review"
+ gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"
+
+ - name: Commit and push validated patch
+ if: steps.validation.outputs.has_changes == 'true'
+ env:
+ BRANCH: ${{ needs.codex_fix.outputs.branch }}
+ GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ run: |
+ set -euo pipefail
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git fetch origin "$BRANCH:refs/remotes/origin/$BRANCH" || true
+ git checkout -B "$BRANCH"
+ git add -A
+ git commit -m "fix: address issue #${ISSUE_NUMBER}"
+ gh auth setup-git
+ git push --force-with-lease --set-upstream origin "$BRANCH"
+
+ - name: Create draft pull request
+ id: pr
+ if: steps.validation.outputs.has_changes == 'true'
+ env:
+ BRANCH: ${{ needs.codex_fix.outputs.branch }}
+ GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md")"
+ cat > "$RUNNER_TEMP/pr-body.md" <> "$GITHUB_OUTPUT"
+
+ pr_number="${pr_url##*/}"
+ gh pr edit "$pr_number" --repo "$REPO" --add-label "ai-generated"
+
+ - name: Update issue with analysis and pull request
+ if: steps.validation.outputs.has_changes == 'true'
+ env:
+ GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ PR_URL: ${{ steps.pr.outputs.url }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md")"
+ body="### Automated fix prepared
+
+ ${report}
+
+ **Draft pull request:** ${PR_URL}
+
+ The pull request remains in draft pending human review and CI."
+ for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --remove-label "$label" || true
+ done
+ gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
+ --add-label "ai: pr-created"
+ gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"