GitHub issues and pull requests¶
Curupira discovers GitHub issues and pull requests with typed TOML filters, compiles them
into GitHub Search
queries, and runs those queries through the GraphQL API. Authentication is a bearer token
from gh auth token. Curupira does not create, edit, close, or comment on issues or pull
requests.
Each match becomes a task: a typed item (issue or pull request fields), an identity used for deduplication, a rendered prompt for a coding-agent CLI, and a local Git checkout or worktree where that agent runs.
| Trigger type | What it discovers |
|---|---|
plugins-py/github-issues |
Issues in one owner/repository |
plugins-py/github-pull-requests |
Pull requests in one owner/repository |
plugins-py/github-project-items |
Issues and pull requests in a column of a GitHub Project board. See GitHub Projects |
Prerequisites¶
- Install the GitHub CLI (
gh). - Authenticate once:
- Confirm
ghhas a token (Curupira reads it with the same command):
Curupira does not store GitHub credentials. It runs gh auth token (cached for five minutes)
and sends that bearer token to https://api.github.com/graphql. Clone and worktree
operations use native git and your normal Git credentials for the repository remote, not
gh repo clone.
Token scopes¶
Use a token with the repo scope for private repositories, or public_repo when you only
need public repositories. Check gh auth status for the scopes on your current login.
Curupira itself only reads Search results; the coding agent it launches may do more,
depending on its profile. See
GitHub's GraphQL authentication guide
for how requested data maps to scopes.
Minimal issue automation¶
In an automation, repo is the GitHub owner/name that Curupira searches. repository is
the alias of a [repositories.<alias>] checkout where the agent runs. They can differ.
[repositories.api]
remote = "https://github.com/acme/api.git"
[agents.defaults]
profile = "opencode-default"
timezone = "UTC"
[agents.profiles.opencode-default]
provider = "opencode"
[automations.resolve-ready-issues]
trigger_type = "plugins-py/github-issues"
repository = "api"
repo = "acme/api"
labels = ["agent-ready"]
linked_pull_request = false
prompt = "Resolve issue ${issue_number}: ${issue_title}\n\n${issue_body}"
Validate without contacting GitHub:
Minimal pull-request automation¶
repo and repository mean the same as in Minimal issue automation.
[repositories.api]
remote = "https://github.com/acme/api.git"
[agents.defaults]
profile = "opencode-default"
timezone = "UTC"
[agents.profiles.opencode-default]
provider = "opencode"
[automations.review-pull-requests]
trigger_type = "plugins-py/github-pull-requests"
repository = "api"
repo = "acme/api"
labels = ["review-needed"]
draft = false
prompt = """
Review pull request ${pull_request_number}: ${pull_request_title}
${pull_request_body}
Branch: ${pull_request_head_ref} -> ${pull_request_base_ref}
"""
Dependabot pull-request automation¶
Filter to one author with author = "app/dependabot" and require mergeable = true so
conflicting pull requests are left alone. The prompt below only uses placeholders listed above,
and ${repo} carries the owner/name used for every gh command. The agent profile needs
auto_approve = true so unattended gh writes (push, gh pr merge) do not stop for approval.
[automations.merge-dependabot-pull-requests]
trigger_type = "plugins-py/github-pull-requests"
repository = "curupira"
repo = "caipora-labs/curupira"
author = "app/dependabot"
state = "open"
draft = false
mergeable = true
sort = "created-asc"
prompt = """
Analyze Dependabot pull request #${pull_request_number} (${pull_request_title}) in ${repo}.
Fix what the update breaks, wait for the checks, then squash-merge it with
`gh pr merge ${pull_request_number} --repo ${repo} --squash`.
"""
Run it until the open Dependabot pull requests are handled:
First run¶
- Install and authenticate the coding-agent CLI named by your profile (
opencodein the snippets above). Provider-native options and setup notes are on the Providers and agents pages. - Preview one matching task without cloning or starting an agent:
- Drain currently available tasks once, or poll continuously:
--dry-run cannot be combined with --watch or --size. validate checks TOML shape
and trigger configuration without calling GitHub; run and run --watch need gh and the
chosen agent CLI installed and authenticated.
Configuration fields¶
Models reject unknown keys.
Shared automation fields¶
| Field | Default | Notes |
|---|---|---|
trigger_type |
see notes | Set explicitly to "plugins-py/github-issues" or "plugins-py/github-pull-requests". If omitted when parsing TOML, Curupira chooses "plugins-py/github-issues" only when the table has no schedule; a schedule key defaults to "cron" instead |
repository |
(required) | Alias of a [repositories.<alias>] entry |
checkout |
"worktree" |
"worktree" or "main" (shared checkout, not a branch name) |
prompt |
(required) | Non-empty string.Template with ${placeholders} |
profile |
None |
Named CLI profile; otherwise agents.defaults.profile, which defaults to "opencode". See Providers and agents for profile options |
Shared GitHub filters¶
Compiled into the Search query.
| Field | Default | Search qualifier / behavior |
|---|---|---|
repo |
(required) | repo:owner/name (owner/repository format) |
state |
"open" |
"open" → is:open; "closed" → is:closed; "all" → neither. For pull requests, is:closed includes merged PRs |
labels |
() |
One label or a list. Each value → label:… (AND: every label must be present) |
exclude_labels |
() |
One label or a list. Each value → -label:… |
assignee |
None |
Login, @me, none → no:assignee, or any → assignee:* |
author |
None |
author:… |
milestone |
None |
milestone:… |
project |
None |
project:<value>, for example acme/5 |
sort |
"created-asc" |
Appended as sort:…. Allowed: created-asc, created-desc, updated-asc, updated-desc, comments-asc, comments-desc |
The project qualifier only restricts which project an item belongs to. To pick items by
board column, use github-project-items. Values with whitespace or ", :, , are quoted in the
compiled query.
Issue-only fields¶
| Field | Default | Behavior |
|---|---|---|
linked_pull_request |
None |
true → linked:pr; false → -linked:pr; omit for no filter |
Pull-request-only fields¶
| Field | Default | Behavior |
|---|---|---|
draft |
None |
true → draft:true; false → draft:false |
base |
None |
base:… |
head |
None |
head:… |
review |
None |
review:… — none, required, approved, or changes_requested |
ci_status |
None |
status:… — success, failure, or pending |
linked_issue |
None |
true → linked:issue; false → -linked:issue |
mergeable |
None |
GraphQL post-filter: true keeps MERGEABLE; false keeps CONFLICTING |
merge_state |
() |
GraphQL post-filter: keep only listed mergeStateStatus values (BEHIND, BLOCKED, CLEAN, DIRTY, DRAFT, HAS_HOOKS, UNKNOWN, UNSTABLE) |
mergeable and merge_state are applied after Search returns results; they are not Search
qualifiers.
Related polling settings¶
Under [settings.polling]:
| Field | Default |
|---|---|
poll_interval_seconds |
30.0 |
batch_size |
100 (maximum 1000) |
cron_poll_interval_seconds |
1.0 |
How issues and pull requests are selected¶
Every poll builds a query that always starts with repo:<repo> and either is:issue or
is:pr, then appends the filters above and sort:<sort>. Example for the minimal issue
automation:
Discovery uses GraphQL Search (ISSUE search type covers both issues and pull requests).
There is no free-form query field: filters are typed TOML only.
Deduplication¶
The host identifies a result by hashing the key/value list the trigger returned, in that order, together with the automation, repository, and trigger type. Curupira keeps no history of issues or pull requests:
- No duplicates within a run. A running
run --watchortuidispatches each task once, whether it finishes or fails. A later poll that returns the same pairs is skipped until the process exits. - Nothing is remembered across runs. Curupira does not record finished issues or pull requests. After a restart, every item that still matches the trigger filters is dispatched again, so the filters decide what is due.
- Interrupted work resumes. This is the only task state Curupira keeps. The coding agent's
session id is saved in
state_db_pathas soon as the agent announces it, and removed when the task finishes successfully. When Curupira restarts in the middle of a task (or the task failed or timed out), the next dispatch continues that session with the same instructions instead of starting over. A changed automation prompt starts a new session.
Changing any returned value, including the title, body, or labels, makes a different task. For
that reason the GitHub triggers do not return values that change by themselves or because the
agent worked on the item (comments, update time, CI and review state, linked pull requests):
the agent would be started again for the same issue. When the agent needs them, tell it to read
them, for example with gh issue view ${issue_number} --comments.
The key is per automation, so two automations may process the same issue or pull request with
different prompts. run --dry-run previews without dispatching.
Keeping finished items out
Filter on state the agent changes, and have the prompt tell the agent to change it when it
is done: close the item, add or remove a label (exclude_labels = ["blocked"]), or link a
pull request (linked_pull_request = false).
When a full cycle finds nothing (or discovery fails), the feed waits
poll_interval_seconds, then doubles the wait on consecutive empty cycles up to five
minutes (300 seconds). Any successful discovery resets the interval.
What the agent prompt receives¶
The prompt can use ${field} or Jinja syntax, see Writing prompts. Every field
of issues and pull requests (title, body, labels, assignees, milestone, diff size, and more) is listed in the Prompt field reference.
Security: issue and pull-request text is untrusted¶
Titles and bodies (${issue_title}, ${issue_body}, ${pull_request_title},
${pull_request_body}, ${item_body}) come from anyone who can open an issue or pull request and are
placed in the coding agent's prompt, so a hostile author can try to steer the agent
(prompt injection). The agent can run destructive actions in the checkout it works in,
so the risk depends on what you allow in the agent profile. Restrict what reaches the
agent with labels (applied only by people you trust), author or assignee, and
review what your agent profile is allowed to run.
Checkout and worktrees¶
GitHub triggers use the default native Git version control.
- Curupira clones
repositories.<alias>.remoteintoworkspace_dir/<alias>(or the aliaspath) on first use. - With
checkout = "worktree"(default), it fetchesorigin, resolves the remote default branch, and adds a worktree at<checkout>.worktrees/<automation_id>/<task_type>-<sha256(task_id)>on branchcurupira/<automation_id>/<task_type>-<sha256(task_id)>. The branch is not pushed. - With
checkout = "main", the agent runs on the shared checkout as-is: no fetch, pull, or branch switch. - After the agent finishes, Curupira removes the worktree and deletes the local branch (cleanup failures are logged, not fatal).
Optional setup_script on the repository alias runs only after a fresh clone of the base
checkout, not inside the worktree.
Agent isolation¶
The agent process runs with its working directory and PWD set to the worktree, and without
the GIT_DIR, GIT_WORK_TREE, and GIT_INDEX_FILE variables that would point Git at another
repository. Your own checkout is never switched by an automation. OpenCode 1.x also receives
--dir <worktree>. OpenCode 2.x has no --dir; a headless run uses the worktree as its working
directory and passes --standalone, so the task does not join the shared background service.
OpenCode 1.x also gets OPENCODE_DB set to <worktree>.opencode.db, so its sessions and project
entries stay with that task. OpenCode 2.x keeps your database, where it stores provider
credentials. Other agent-wide state, such as the global OpenCode configuration, is shared.
Common errors¶
| Symptom | Likely cause | Fix |
|---|---|---|
Required executable not found: gh |
GitHub CLI missing from PATH |
Install gh and retry |
gh exited with status … / empty token |
Not logged in, or token unavailable | Run gh auth login, then gh auth token |
| GraphQL / HTTP 401 or scope errors | Token lacks access to the repository or Search | Re-run gh auth login so the token includes repo, or use a classic PAT with the scopes required by GitHub's GraphQL guide |
| No tasks scheduled | Filters match nothing | Widen labels/state, drop linked_* / draft filters, or confirm repo is correct; empty cycles are not an error |
GitHub GraphQL temporarily failed with status 429 (or similar) |
Rate limit or transient API failure | Curupira retries transient errors a few times; back off or reduce poll frequency / batch_size |
Discovery warnings in run --watch |
GraphQL or CLI failure treated as an empty cycle | Check logs for Discovery failed for …; fix auth or network, then wait for the next poll |
curupira validate checks TOML shape and trigger configuration without calling GitHub.
curupira run and curupira run --watch need gh installed and authenticated.