Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .factory/prompts/crash/link-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Crash Issue Linking

You are linking a crash to potentially related GitHub issues so human reviewers can quickly validate whether a fix may resolve multiple reports.

## Inputs

Before starting, you should have:

1. **Crash report** (from `script/sentry-fetch <issue-id>` or Sentry MCP)
2. **ANALYSIS.md** from investigation phase, including root cause and crash site

If either is missing, stop and report what is missing.

## Goal

Search GitHub issues and produce a reviewer-ready shortlist grouped by confidence:

- **High confidence**
- **Medium confidence**
- **Low confidence**

The output is advisory only. Humans must confirm before adding closing keywords or making release claims.

## Workflow

### Step 1: Build Search Signals

Extract concrete signals from the crash + analysis:

1. Crash site function, file, and crate
2. Error message / panic text
3. Key stack frames (especially in-app)
4. Reproduction trigger phrasing (user actions)
5. Affected platform/version tags if available

### Step 2: Search GitHub Issues

Search **only** issues in `zed-industries/zed` (prefer `gh issue list` / `gh issue view` / GraphQL if available) by:

1. Panic/error text
2. Function/file names
3. Crate/module names + symptom keywords
4. Similar reproduction patterns

Check both open and recently closed issues in `zed-industries/zed`.

### Step 3: Score Confidence

Assign confidence based on evidence quality:

- **High:** direct technical overlap (same crash site or same invariant violation with matching repro language)
- **Medium:** partial overlap (same subsystem and symptom, but indirect stack/repro match)
- **Low:** thematic similarity only (same area/keywords without solid technical match)

Avoid inflated confidence. If uncertain, downgrade.

### Step 4: Produce Structured Output

Write `LINKED_ISSUES.md` using this exact structure:

```markdown
# Potentially Related GitHub Issues

## High Confidence
- [#12345](https://github.com/zed-industries/zed/issues/12345) — <title>
- Why: <1-2 sentence evidence-backed rationale>
- Evidence: <stack frame / error text / repro alignment>

## Medium Confidence
- ...

## Low Confidence
- ...

## Reviewer Checklist
- [ ] Confirm High confidence issues should be referenced in PR body
- [ ] Confirm any issue should receive closing keywords (`Fixes #...`)
- [ ] Reject false positives before merge
```

If no credible matches are found, keep sections present and write `- None found` under each.

## Rules

- Do not fabricate issues or URLs.
- Do not include issues from any repository other than `zed-industries/zed`.
- Do not add closing keywords automatically.
- Keep rationale short and evidence-based.
- Favor precision over recall.
284 changes: 284 additions & 0 deletions .github/workflows/background_agent_mvp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
name: background_agent_mvp

on:
schedule:
- cron: "0 16 * * 1-5"
workflow_dispatch:
inputs:
crash_ids:
description: "Optional comma-separated Sentry issue IDs (e.g. ZED-4VS,ZED-123)"
required: false
type: string
reviewers:
description: "Optional comma-separated GitHub reviewer handles"
required: false
type: string
top:
description: "Top N candidates when crash_ids is empty"
required: false
type: string
default: "3"

permissions:
contents: write
pull-requests: write

env:
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
DROID_MODEL: claude-opus-4-5-20251101
SENTRY_ORG: zed-dev

jobs:
run-mvp:
runs-on: ubuntu-latest
timeout-minutes: 180

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Droid CLI
run: |
curl -fsSL https://app.factory.ai/cli | sh
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
echo "DROID_BIN=${HOME}/.local/bin/droid" >> "$GITHUB_ENV"
"${HOME}/.local/bin/droid" --version

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Resolve reviewers
id: reviewers
env:
INPUT_REVIEWERS: ${{ inputs.reviewers }}
DEFAULT_REVIEWERS: ${{ vars.BACKGROUND_AGENT_REVIEWERS }}
run: |
set -euo pipefail
if [ -z "$DEFAULT_REVIEWERS" ]; then
DEFAULT_REVIEWERS="eholk,morgankrey,osiewicz,bennetbo"
fi
REVIEWERS="${INPUT_REVIEWERS:-$DEFAULT_REVIEWERS}"
REVIEWERS="$(echo "$REVIEWERS" | tr -d '[:space:]')"
echo "reviewers=$REVIEWERS" >> "$GITHUB_OUTPUT"

- name: Select crash candidates
id: candidates
env:
INPUT_CRASH_IDS: ${{ inputs.crash_ids }}
INPUT_TOP: ${{ inputs.top }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_BACKGROUND_AGENT_MVP_TOKEN }}
run: |
set -euo pipefail

PREFETCH_DIR="/tmp/crash-data"
ARGS=(--select-only --prefetch-dir "$PREFETCH_DIR" --org "$SENTRY_ORG")
if [ -n "$INPUT_CRASH_IDS" ]; then
ARGS+=(--crash-ids "$INPUT_CRASH_IDS")
else
TARGET_DRAFT_PRS="${INPUT_TOP:-3}"
if ! [[ "$TARGET_DRAFT_PRS" =~ ^[0-9]+$ ]] || [ "$TARGET_DRAFT_PRS" -lt 1 ]; then
TARGET_DRAFT_PRS="3"
fi
CANDIDATE_TOP=$((TARGET_DRAFT_PRS * 5))
if [ "$CANDIDATE_TOP" -gt 100 ]; then
CANDIDATE_TOP=100
fi
ARGS+=(--top "$CANDIDATE_TOP" --sample-size 100)
fi

IDS="$(python3 script/run-background-agent-mvp-local "${ARGS[@]}")"

if [ -z "$IDS" ]; then
echo "No candidates selected"
exit 1
fi

echo "Using crash IDs: $IDS"
echo "ids=$IDS" >> "$GITHUB_OUTPUT"

- name: Run background agent pipeline per crash
id: pipeline
env:
GH_TOKEN: ${{ github.token }}
REVIEWERS: ${{ steps.reviewers.outputs.reviewers }}
CRASH_IDS: ${{ steps.candidates.outputs.ids }}
TARGET_DRAFT_PRS_INPUT: ${{ inputs.top }}
run: |
set -euo pipefail

git config user.name "factory-droid[bot]"
git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"

# Crash ID format validation regex
CRASH_ID_PATTERN='^[A-Za-z0-9]+-[A-Za-z0-9]+$'
TARGET_DRAFT_PRS="${TARGET_DRAFT_PRS_INPUT:-3}"
if ! [[ "$TARGET_DRAFT_PRS" =~ ^[0-9]+$ ]] || [ "$TARGET_DRAFT_PRS" -lt 1 ]; then
TARGET_DRAFT_PRS="3"
fi
CREATED_DRAFT_PRS=0

IFS=',' read -r -a CRASH_ID_ARRAY <<< "$CRASH_IDS"

for CRASH_ID in "${CRASH_ID_ARRAY[@]}"; do
if [ "$CREATED_DRAFT_PRS" -ge "$TARGET_DRAFT_PRS" ]; then
echo "Reached target draft PR count ($TARGET_DRAFT_PRS), stopping candidate processing"
break
fi

CRASH_ID="$(echo "$CRASH_ID" | xargs)"
[ -z "$CRASH_ID" ] && continue

# Validate crash ID format to prevent injection via branch names or prompts
if ! [[ "$CRASH_ID" =~ $CRASH_ID_PATTERN ]]; then
echo "ERROR: Invalid crash ID format: '$CRASH_ID' — skipping"
continue
fi

BRANCH="background-agent/mvp-${CRASH_ID,,}-$(date +%Y%m%d)"
echo "Running crash pipeline for $CRASH_ID on $BRANCH"

# Deduplication: skip if a draft PR already exists for this crash
EXISTING_BRANCH_PR="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' || echo "")"
if [ -n "$EXISTING_BRANCH_PR" ]; then
echo "Draft PR #$EXISTING_BRANCH_PR already exists for $CRASH_ID — skipping"
continue
fi

git fetch origin main
git checkout -B "$BRANCH" origin/main

CRASH_DATA_FILE="/tmp/crash-data/crash-${CRASH_ID}.md"
if [ ! -f "$CRASH_DATA_FILE" ]; then
echo "WARNING: No pre-fetched crash data for $CRASH_ID at $CRASH_DATA_FILE — skipping"
continue
fi

python3 -c "
import sys
crash_id, data_file = sys.argv[1], sys.argv[2]
prompt = f'''You are running the weekly background crash-fix MVP pipeline for crash {crash_id}.

The crash report has been pre-fetched and is available at: {data_file}
Read this file to get the crash data. Do not call script/sentry-fetch.

Required workflow:
1. Read the crash report from {data_file}
2. Follow .factory/prompts/crash/investigate.md and write ANALYSIS.md
3. Follow .factory/prompts/crash/link-issues.md and write LINKED_ISSUES.md
4. Follow .factory/prompts/crash/fix.md to implement a minimal fix with tests
5. Run validators required by the fix prompt for the affected crate(s)
6. Write PR_BODY.md with sections:
- Crash Summary
- Root Cause
- Fix
- Validation
- Potentially Related Issues (High/Medium/Low from LINKED_ISSUES.md)
- Reviewer Checklist

Constraints:
- Do not merge or auto-approve.
- Keep changes narrowly scoped to this crash.
- Do not modify files in .github/, .factory/, or script/ directories.
- When investigating git history, limit your search to the last 2 weeks of commits. Do not traverse older history.
- If the crash is not solvable with available context, write a clear blocker summary to PR_BODY.md.
'''
import textwrap
with open('/tmp/background-agent-prompt.md', 'w') as f:
f.write(textwrap.dedent(prompt))
" "$CRASH_ID" "$CRASH_DATA_FILE"

if ! "$DROID_BIN" exec --auto medium -m "$DROID_MODEL" -f /tmp/background-agent-prompt.md; then
echo "Droid execution failed for $CRASH_ID, continuing to next candidate"
continue
fi

for REPORT_FILE in ANALYSIS.md LINKED_ISSUES.md PR_BODY.md; do
if [ -f "$REPORT_FILE" ]; then
echo "::group::${CRASH_ID} ${REPORT_FILE}"
cat "$REPORT_FILE"
echo "::endgroup::"
fi
done

if git diff --quiet; then
echo "No code changes produced for $CRASH_ID"
continue
fi

# Stage only expected file types — not git add -A
git add -- '*.rs' '*.toml' 'Cargo.lock' 'ANALYSIS.md' 'LINKED_ISSUES.md' 'PR_BODY.md'

# Reject changes to protected paths
PROTECTED_CHANGES="$(git diff --cached --name-only | grep -E '^(\.github/|\.factory/|script/)' || true)"
if [ -n "$PROTECTED_CHANGES" ]; then
echo "ERROR: Agent modified protected paths — aborting commit for $CRASH_ID:"
echo "$PROTECTED_CHANGES"
git reset HEAD -- .
continue
fi

if ! git diff --cached --quiet; then
git commit -m "fix: draft crash fix for ${CRASH_ID}"
fi

git push -u origin "$BRANCH"

TITLE="fix: draft crash fix for ${CRASH_ID}"
BODY_FILE="PR_BODY.md"
if [ ! -f "$BODY_FILE" ]; then
BODY_FILE="/tmp/pr-body-${CRASH_ID}.md"
printf "Automated draft crash-fix pipeline output for %s.\n\nNo PR_BODY.md was generated by the agent; please review commit and linked artifacts manually.\n" "$CRASH_ID" > "$BODY_FILE"
fi

EXISTING_PR="$(gh pr list --head "$BRANCH" --json number --jq '.[0].number')"
if [ -n "$EXISTING_PR" ]; then
gh pr edit "$EXISTING_PR" --title "$TITLE" --body-file "$BODY_FILE"
PR_NUMBER="$EXISTING_PR"
else
PR_URL="$(gh pr create --draft --base main --head "$BRANCH" --title "$TITLE" --body-file "$BODY_FILE")"
PR_NUMBER="$(basename "$PR_URL")"
fi

if [ -n "$REVIEWERS" ]; then
IFS=',' read -r -a REVIEWER_ARRAY <<< "$REVIEWERS"
for REVIEWER in "${REVIEWER_ARRAY[@]}"; do
[ -z "$REVIEWER" ] && continue
gh pr edit "$PR_NUMBER" --add-reviewer "$REVIEWER" || true
done
fi

CREATED_DRAFT_PRS=$((CREATED_DRAFT_PRS + 1))
echo "Created/updated draft PRs this run: $CREATED_DRAFT_PRS/$TARGET_DRAFT_PRS"
done

echo "created_draft_prs=$CREATED_DRAFT_PRS" >> "$GITHUB_OUTPUT"
echo "target_draft_prs=$TARGET_DRAFT_PRS" >> "$GITHUB_OUTPUT"

- name: Cleanup pre-fetched crash data
if: always()
run: rm -rf /tmp/crash-data

- name: Workflow summary
if: always()
env:
SUMMARY_CRASH_IDS: ${{ steps.candidates.outputs.ids }}
SUMMARY_REVIEWERS: ${{ steps.reviewers.outputs.reviewers }}
SUMMARY_CREATED_DRAFT_PRS: ${{ steps.pipeline.outputs.created_draft_prs }}
SUMMARY_TARGET_DRAFT_PRS: ${{ steps.pipeline.outputs.target_draft_prs }}
run: |
{
echo "## Background Agent MVP"
echo ""
echo "- Crash IDs: ${SUMMARY_CRASH_IDS:-none}"
echo "- Reviewer routing: ${SUMMARY_REVIEWERS:-NOT CONFIGURED}"
echo "- Draft PRs created: ${SUMMARY_CREATED_DRAFT_PRS:-0}/${SUMMARY_TARGET_DRAFT_PRS:-3}"
echo "- Pipeline: investigate -> link-issues -> fix -> draft PR"
} >> "$GITHUB_STEP_SUMMARY"

concurrency:
group: background-agent-mvp
cancel-in-progress: false
Loading