GitOps configuration
.pullpulse.yml
Commit this at the root of a repository. It is validated on every push to the default branch, and a mistake comes back as a check run annotation on the commit that made it — not as silence. This page is generated from the same schema that does the validating, so it cannot drift from the parser.
Complete example
Every section spelled out with its real default, so you can delete what you do not need rather than guess at what exists. Rules and channel names are the only invented values.
version: 1
defaults:
channel: "#eng-reviews"
strategy: message
mention: reviewers
holdUntil: readyForReview
threadReplies: true
aiBrief: true
escalateAfter: "8h"
quietHours:
tz: Europe/Berlin
from: "19:00"
to: "08:00"
quietDays: [6, 7]
rules:
- name: "agent PRs wait for green CI"
when:
authorKind: agent
route:
strategy: rollup
mention: none
holdUntil: checksGreen
digest: hourly
- name: "migrations go to the database channel"
when:
paths: ["**/migrations/**", "**/*.sql"]
route:
channel: "#db-review"
mention: reviewers
escalateAfter: "4h"
- name: "high risk always pings a human"
when:
riskAtLeast: 70
route:
channel: "#eng-reviews"
mention: reviewers
escalateAfter: "2h"
aiBrief: true
reminders:
enabled: true
firstNudgeAfter: "4h"
escalateAfter: "24h"
staleAfter: "3d"
maxPerReviewerPerDay: 6
escalateToOwners: true
ai:
enabled: true
provider: workers-ai
brief: true
reviewerSuggestions: true
riskAdjustment: true
pulseReport: false
maxDiffBytes: 180000
digests:
enabled: true
cadence: daily
at: "09:30"
tz: Europe/Berlin
channel: "#eng-reviews"
includeAgentQueue: true
agents:
enabled: true
extraLogins: [our-internal-bot]
minRiskToNotify: 35
holdUntil: checksGreen
rollup: true
ignoreRepos: ["acme/legacy-*"]
ignorePaths: ["**/*.md", "**/fixtures/**"]
Rules are first-match-wins, evaluated top to bottom, with the workspace defaults underneath. /pullpulse explain <pr> prints which rule matched and why. Set continue: true on a rule to keep evaluating after it matches.
Durations match ^(\d+)(ms|s|m|h|d|w)$ — 30m, 4h, 2d. Times of day are 24-hour HH:MM in the accompanying tz. Channels are #name or a Slack channel id. Path and repo patterns are globs, where ** crosses directory boundaries.
version
| key | type | default | notes |
|---|
| version | 1 | required | |
defaults
| key | type | default | notes |
|---|
| defaults | object | — | section |
| defaults.channel | string | — | matches ^(#[a-z0-9][a-z0-9._-]{0,79}|[CGD][A-Z0-9]{6,})$ |
| defaults.strategy | message | rollup | digest | mute | "message" | |
| defaults.mention | none | author | reviewers | all | "reviewers" | |
| defaults.holdUntil | none | checksGreen | checksComplete | readyForReview | "readyForReview" | |
| defaults.threadReplies | boolean | true | |
| defaults.aiBrief | boolean | true | |
| defaults.escalateAfter | string | — | |
| defaults.quietHours | object | — | section |
| defaults.quietHours.tz | string | "UTC" | |
| defaults.quietHours.from | string | required | matches ^([01]\d|2[0-3]):[0-5]\d$ |
| defaults.quietHours.to | string | required | matches ^([01]\d|2[0-3]):[0-5]\d$ |
| defaults.quietHours.quietDays | integer[] | [6,7] | up to 7 entries |
rules
| key | type | default | notes |
|---|
| rules | object[] | [] | up to 200 entries |
| rules[].name | string | required | |
| rules[].when | object | required | section |
| rules[].when.repos | string[] | — | at least one entry |
| rules[].when.events | pr.opened | pr.reopened | pr.ready_for_review | pr.converted_to_draft | pr.synchronized | pr.edited | pr.labeled | pr.unlabeled | pr.review_requested | pr.review_request_removed | pr.assigned | pr.merged | pr.closed | review.submitted | review.dismissed | comment.created | comment.edited | checks.updated | config.updated[] | — | at least one entry |
| rules[].when.authorKind | human | agent | bot | human | agent | bot[] | — | |
| rules[].when.agents | claude-code | github-copilot | openai-codex | devin | cursor | jules | aider | gemini-cli | unknown-agent[] | — | at least one entry |
| rules[].when.authors | string[] | — | at least one entry |
| rules[].when.labels | string[] | — | at least one entry |
| rules[].when.notLabels | string[] | — | at least one entry |
| rules[].when.baseRef | string[] | — | at least one entry |
| rules[].when.headRef | string[] | — | at least one entry |
| rules[].when.paths | string[] | — | at least one entry |
| rules[].when.draft | boolean | — | |
| rules[].when.riskAtLeast | integer | — | 0–100 |
| rules[].when.riskBelow | integer | — | 0–100 |
| rules[].when.sizeAtLeast | integer | — | 0–9007199254740991 |
| rules[].when.sizeBelow | integer | — | 1–9007199254740991 |
| rules[].when.titleMatches | string | — | |
| rules[].when.checks | green | red | pending | none[] | — | at least one entry |
| rules[].route | object | required | section |
| rules[].route.channel | string | — | matches ^(#[a-z0-9][a-z0-9._-]{0,79}|[CGD][A-Z0-9]{6,})$ |
| rules[].route.strategy | message | rollup | digest | mute | — | |
| rules[].route.mention | none | author | reviewers | all | — | |
| rules[].route.holdUntil | none | checksGreen | checksComplete | readyForReview | — | |
| rules[].route.digest | hourly | daily | weekly | — | |
| rules[].route.escalateAfter | string | — | |
| rules[].route.threadReplies | boolean | — | |
| rules[].route.aiBrief | boolean | — | |
| rules[].continue | boolean | false | |
reminders
| key | type | default | notes |
|---|
| reminders | object | — | section |
| reminders.enabled | boolean | true | |
| reminders.firstNudgeAfter | string | "4h" | |
| reminders.escalateAfter | string | "24h" | |
| reminders.staleAfter | string | "3d" | |
| reminders.maxPerReviewerPerDay | integer | 6 | 1–50 |
| reminders.escalateToOwners | boolean | true | |
ai
| key | type | default | notes |
|---|
| ai | object | — | section |
| ai.enabled | boolean | true | |
| ai.provider | workers-ai | openai | anthropic | "workers-ai" | |
| ai.model | string | — | |
| ai.brief | boolean | true | |
| ai.reviewerSuggestions | boolean | true | |
| ai.riskAdjustment | boolean | true | |
| ai.pulseReport | boolean | false | |
| ai.maxDiffBytes | integer | 180000 | 1000–2000000 |
digests
| key | type | default | notes |
|---|
| digests | object | — | section |
| digests.enabled | boolean | true | |
| digests.cadence | hourly | daily | weekly | "daily" | |
| digests.at | string | "09:30" | matches ^([01]\d|2[0-3]):[0-5]\d$ |
| digests.tz | string | "UTC" | |
| digests.channel | string | — | matches ^(#[a-z0-9][a-z0-9._-]{0,79}|[CGD][A-Z0-9]{6,})$ |
| digests.includeAgentQueue | boolean | true | |
agents
| key | type | default | notes |
|---|
| agents | object | — | section |
| agents.enabled | boolean | true | |
| agents.extraLogins | string[] | [] | |
| agents.minRiskToNotify | integer | 35 | 0–100 |
| agents.holdUntil | none | checksGreen | checksComplete | readyForReview | "checksGreen" | |
| agents.rollup | boolean | true | |
ignoreRepos
| key | type | default | notes |
|---|
| ignoreRepos | string[] | [] | |
ignorePaths
| key | type | default | notes |
|---|
| ignorePaths | string[] | [] | |