PullPulse
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

keytypedefaultnotes
version1required

defaults

keytypedefaultnotes
defaultsobjectsection
defaults.channelstringmatches ^(#[a-z0-9][a-z0-9._-]{0,79}|[CGD][A-Z0-9]{6,})$
defaults.strategymessage | rollup | digest | mute"message"
defaults.mentionnone | author | reviewers | all"reviewers"
defaults.holdUntilnone | checksGreen | checksComplete | readyForReview"readyForReview"
defaults.threadRepliesbooleantrue
defaults.aiBriefbooleantrue
defaults.escalateAfterstring
defaults.quietHoursobjectsection
defaults.quietHours.tzstring"UTC"
defaults.quietHours.fromstringrequiredmatches ^([01]\d|2[0-3]):[0-5]\d$
defaults.quietHours.tostringrequiredmatches ^([01]\d|2[0-3]):[0-5]\d$
defaults.quietHours.quietDaysinteger[][6,7]up to 7 entries

rules

keytypedefaultnotes
rulesobject[][]up to 200 entries
rules[].namestringrequired
rules[].whenobjectrequiredsection
rules[].when.reposstring[]at least one entry
rules[].when.eventspr.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.authorKindhuman | agent | bot | human | agent | bot[]
rules[].when.agentsclaude-code | github-copilot | openai-codex | devin | cursor | jules | aider | gemini-cli | unknown-agent[]at least one entry
rules[].when.authorsstring[]at least one entry
rules[].when.labelsstring[]at least one entry
rules[].when.notLabelsstring[]at least one entry
rules[].when.baseRefstring[]at least one entry
rules[].when.headRefstring[]at least one entry
rules[].when.pathsstring[]at least one entry
rules[].when.draftboolean
rules[].when.riskAtLeastinteger0–100
rules[].when.riskBelowinteger0–100
rules[].when.sizeAtLeastinteger0–9007199254740991
rules[].when.sizeBelowinteger1–9007199254740991
rules[].when.titleMatchesstring
rules[].when.checksgreen | red | pending | none[]at least one entry
rules[].routeobjectrequiredsection
rules[].route.channelstringmatches ^(#[a-z0-9][a-z0-9._-]{0,79}|[CGD][A-Z0-9]{6,})$
rules[].route.strategymessage | rollup | digest | mute
rules[].route.mentionnone | author | reviewers | all
rules[].route.holdUntilnone | checksGreen | checksComplete | readyForReview
rules[].route.digesthourly | daily | weekly
rules[].route.escalateAfterstring
rules[].route.threadRepliesboolean
rules[].route.aiBriefboolean
rules[].continuebooleanfalse

reminders

keytypedefaultnotes
remindersobjectsection
reminders.enabledbooleantrue
reminders.firstNudgeAfterstring"4h"
reminders.escalateAfterstring"24h"
reminders.staleAfterstring"3d"
reminders.maxPerReviewerPerDayinteger61–50
reminders.escalateToOwnersbooleantrue

ai

keytypedefaultnotes
aiobjectsection
ai.enabledbooleantrue
ai.providerworkers-ai | openai | anthropic"workers-ai"
ai.modelstring
ai.briefbooleantrue
ai.reviewerSuggestionsbooleantrue
ai.riskAdjustmentbooleantrue
ai.pulseReportbooleanfalse
ai.maxDiffBytesinteger1800001000–2000000

digests

keytypedefaultnotes
digestsobjectsection
digests.enabledbooleantrue
digests.cadencehourly | daily | weekly"daily"
digests.atstring"09:30"matches ^([01]\d|2[0-3]):[0-5]\d$
digests.tzstring"UTC"
digests.channelstringmatches ^(#[a-z0-9][a-z0-9._-]{0,79}|[CGD][A-Z0-9]{6,})$
digests.includeAgentQueuebooleantrue

agents

keytypedefaultnotes
agentsobjectsection
agents.enabledbooleantrue
agents.extraLoginsstring[][]
agents.minRiskToNotifyinteger350–100
agents.holdUntilnone | checksGreen | checksComplete | readyForReview"checksGreen"
agents.rollupbooleantrue

ignoreRepos

keytypedefaultnotes
ignoreReposstring[][]

ignorePaths

keytypedefaultnotes
ignorePathsstring[][]