Poll GitHub issues from a single project workspace and auto-fix them with a local Cursor agent (via @cursor/sdk). Each project gets its own config — a watcher in project-a never touches project-b.
- Polls GitHub every 60s (configurable) for open issues
- Creates a fix branch:
fix/issue-123-short-title - Runs a local Cursor agent against your repo checkout
- Commits + pushes the branch (never
main/master) - Comments on the issue with testing instructions
From this repo:
cd cursor-event-receiver
npm install
npm linkOr run without linking:
npm run dev -- init
npm run dev -- watchRun these inside the target repo, not inside this tool's repo:
cd ~/path/to/your-project
export GITHUB_PAT=ghp_... # repo + issues write scopes
export CURSOR_API_KEY=cursor_...
cursor-event-receiver initinit writes .cursor/event-receiver.yaml and detects owner/repo from git remote origin when possible.
Example config:
github:
owner: your-org
repo: your-project
pat: ${GITHUB_PAT}
poll:
interval_seconds: 60
issues:
mode: all
labels: []
numbers: []
cursor:
api_key: ${CURSOR_API_KEY}
model: composer-2.5
output:
comment_on_issue: true
git:
branch_prefix: fix/issue-
never_push: [main, master]Add to the project's .gitignore (or run init, which adds these automatically):
.cursor/event-receiver-state.json
.cursor/event-receiver.yaml
The watcher updates .cursor/event-receiver-state.json on every poll (including the first tick when watch starts). If that file is tracked by git, your working tree stays dirty and branch checkout fails with:
Working tree has uncommitted changes. Commit or stash before running the receiver.
Minimum (recommended): ignore receiver config + runtime state:
.cursor/event-receiver-state.json
.cursor/event-receiver.yaml
Alternative: ignore the whole .cursor/ folder if you do not want any Cursor-local files in git:
.cursor/
If you ignore all of .cursor/, run cursor-event-receiver init locally in each clone -- config is not shared via git unless you commit .cursor/event-receiver.yaml separately (safe when secrets use ${ENV} placeholders, not literal tokens).
You can commit .cursor/event-receiver.yaml while still gitignoring the state file — that is the usual setup for a shared repo config without leaking processed-issue history into commits.
If you are already on a branch matching the issue (e.g. fix/issue-26-*), the receiver continues on that branch instead of checking out main and recreating it. This supports retrying after a failed or interrupted run:
git checkout fix/issue-26-enhancement-display-app-version-number-o
cursor-event-receiver run-once --issue 26 --forceWhen resuming, .cursor/event-receiver-state.json changes are ignored for the clean-tree check, and you stay on the fix branch when the run finishes.
After each run, the receiver posts a summary comment on the issue (what it tried, branch name, testing steps). Teammate comments on the issue are scanned on every poll and trigger a resume on the same fix branch.
Flow:
- Receiver fixes issue #26 → posts bot comment with testing instructions
- You test and comment: "Version still doesn't show on the home screen — check
HomeScreen.tsx" - Next poll (or
run-once --issue 26) picks up your comment - Agent resumes on
fix/issue-26-*with your feedback in the prompt - Receiver posts an updated summary comment
Bot comments are ignored (marked with <!-- cursor-event-receiver -->). Only new human comments since the last processed comment ID trigger a rerun.
Optional config in .cursor/event-receiver.yaml:
comments:
scan_feedback: true
feedback_trigger: "" # empty = any human comment; or e.g. "@fix" to require that textExample feedback comment:
The build works but the version text is hidden behind the header. Please adjust the layout.
Or with a trigger set to @fix:
@fix The app crashes on launch when checking out this branch — ENOENT on config path.
Every agent call is built from layered prompts before Agent.prompt() runs:
prompt.preamble— inline rules in yaml (always prepended)prompt.preamble_file— markdown file merged next (default:.cursor/event-receiver-prompt.md)- Mode instructions —
initial(first fix) orfeedback(teammate commented) - Issue body + comment thread
- Requirements — stricter on feedback reruns (must change code, don't argue the fix is already correct)
Add to .cursor/event-receiver.yaml:
prompt:
preamble: |
This is an Electron desktop app. Run npm run dev to test UI changes.
Keep diffs minimal. Match existing TypeScript patterns.
preamble_file: .cursor/event-receiver-prompt.md
initial: |
First attempt — locate the root cause before editing.
feedback: |
A human tester commented below. Their report is ground truth.
Do not explain that the fix already works. Change code to address their feedback.init creates .cursor/event-receiver-prompt.md as an editable template.
Preview the full prompt before running:
cursor-event-receiver print-prompt --issue 26
cursor-event-receiver print-prompt --issue 26 --feedbackFeedback reruns use strong built-in defaults if you leave prompt.feedback empty — tuned so the agent doesn't "correct" the tester and actually edits code.
# Test one issue manually
cursor-event-receiver run-once --issue 42
# Re-run even if already processed
cursor-event-receiver run-once --issue 42 --force
# Start the 60s poll loop
cursor-event-receiver watch
# Show config + processed issues
cursor-event-receiver status
# Preview agent prompt (debug)
cursor-event-receiver print-prompt --issue 42 --feedbackRun one watcher per Cursor project, each from its own directory:
# Terminal 1
cd ~/projects/frontend && cursor-event-receiver watch
# Terminal 2
cd ~/projects/backend && cursor-event-receiver watchEach project reads only its own:
.cursor/event-receiver.yaml.cursor/event-receiver-state.json
For branch push + issue comments:
repo(or fine-grained: Contents read/write, Issues read/write, Metadata read)
- Node.js 20+
- Git repo with
originremote - Clean working tree before each issue run (gitignore
.cursor/event-receiver-state.jsonso polling does not dirty the tree) CURSOR_API_KEYfrom Cursor Dashboard → Integrations- Ripgrep for local Cursor agents (bundled automatically via
@cursor/sdk-*platform packages)
If you see Ripgrep path not configured, reinstall dependencies or set:
export CURSOR_RIPGREP_PATH=/path/to/rg