forked from zed-industries/zed
-
Notifications
You must be signed in to change notification settings - Fork 0
331 lines (282 loc) · 12.9 KB
/
background_agent_mvp.yml
File metadata and controls
331 lines (282 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
name: background_agent_mvp
# NOTE: Scheduled runs disabled as of 2026-02-24. The workflow can still be
# triggered manually via workflow_dispatch. See Notion doc "Background Agent
# for Zed" for current status and contact info to resume this work.
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
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@a26af69be951a213d495a4c3e4e4022e16d87065 # 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
if ! git fetch origin main; then
echo "WARNING: Failed to fetch origin/main for $CRASH_ID — skipping"
continue
fi
if ! git checkout -B "$BRANCH" origin/main; then
echo "WARNING: Failed to create checkout branch $BRANCH for $CRASH_ID — skipping"
continue
fi
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. Read and follow .rules.
3. Follow .factory/prompts/crash/investigate.md and write ANALYSIS.md
4. Follow .factory/prompts/crash/link-issues.md and write LINKED_ISSUES.md
5. Follow .factory/prompts/crash/fix.md to implement a minimal fix with tests
6. Run validators required by the fix prompt for the affected code paths
7. Write PR_BODY.md with sections:
- Crash Summary
- Root Cause
- Fix
- Validation
- Potentially Related Issues (High/Medium/Low from LINKED_ISSUES.md)
- Reviewer Checklist
- Release Notes (final section; format as Release Notes:, then a blank line, then one bullet like - N/A)
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 crash ${CRASH_ID}"
fi
git push -u origin "$BRANCH"
CRATE_PREFIX=""
CHANGED_CRATES="$(git diff --cached --name-only | awk -F/ '/^crates\/[^/]+\// {print $2}' | sort -u)"
if [ -n "$CHANGED_CRATES" ] && [ "$(printf "%s\n" "$CHANGED_CRATES" | wc -l | tr -d ' ')" -eq 1 ]; then
CRATE_PREFIX="${CHANGED_CRATES}: "
fi
TITLE="${CRATE_PREFIX}Fix crash ${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
python3 -c '
import re
import sys
path = sys.argv[1]
body = open(path, encoding="utf-8").read()
pattern = re.compile(r"(^|\n)Release Notes:\r?\n(?:\r?\n)*(?P<bullets>(?:\s*-\s+.*(?:\r?\n|$))+)", re.MULTILINE)
match = pattern.search(body)
if match:
bullets = [
re.sub(r"^\s*", "", bullet)
for bullet in re.findall(r"^\s*-\s+.*$", match.group("bullets"), re.MULTILINE)
]
if not bullets:
bullets = ["- N/A"]
section = "Release Notes:\n\n" + "\n".join(bullets)
body_without_release_notes = (body[: match.start()] + body[match.end() :]).rstrip()
if body_without_release_notes:
normalized_body = f"{body_without_release_notes}\n\n{section}\n"
else:
normalized_body = f"{section}\n"
else:
normalized_body = body.rstrip() + "\n\nRelease Notes:\n\n- N/A\n"
with open(path, "w", encoding="utf-8") as file:
file.write(normalized_body)
' "$BODY_FILE"
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