PullPulse
GitOps configuration

.pullpulse.yml

Set routing, quiet hours and agent handling in your repository. PullPulse validates every change and reports problems as a GitHub check.

Complete .pullpulse.yml example

Start from the defaults, then keep the sections your team needs.

version: 1
defaults:
  channel: "#eng-reviews"
  strategy: message
  mention: reviewers
  holdUntil: readyAndChecksGreen
  threadReplies: true
  aiBrief: true
  notifyAuthorOnReady: true
  density: card
  releases: false
  weeklyNotes: false
  escalateAfter: "8h"
  quietHours:
    tz: Europe/Berlin
    from: "19:00"
    to: "08:00"
    quietDays: [6, 7]
rules:
  - name: "critical workloads need a review thread"
    when:
      workload:
        criticality: critical
    route:
      channel: "#system-review"
      strategy: message
      mention: reviewers
      aiBrief: true
  - name: "migrations go to the database channel"
    when:
      paths: ["**/migrations/**", "**/*.sql"]
    route:
      channel: "#db-review"
      mention: reviewers
      escalateAfter: "4h"
  - name: "agent PRs wait for green CI"
    when:
      authorKind: agent
    route:
      strategy: rollup
      mention: none
      holdUntil: checksGreen
      digest: hourly
reminders:
  enabled: true
  firstNudgeAfter: "4h"
  escalateAfter: "24h"
  staleAfter: "3d"
  maxPerReviewerPerDay: 6
  escalateToOwners: true
ai:
  enabled: true
  provider: workers-ai
  brief: true
  reviewerSuggestions: true
  suggestAttention: 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]
  holdUntil: readyAndChecksGreen
  rollup: true
ignoreRepos: ["acme/legacy-*"]
ignorePaths: ["**/*.md", "**/fixtures/**"]
system:
  workloads:
    - name: sessions
      paths: ["src/auth/**"]
      criticality: critical
      hotPath: true
      intent: "Validate the session before returning private workspace data."

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
defaultsobject—section
defaults.channelstring—matches ^(#[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 | readyAndChecksGreen"readyAndChecksGreen"
defaults.threadRepliesbooleantrue
defaults.aiBriefbooleantrue
defaults.notifyAuthorOnReadybooleantrue
defaults.densitycard | compact | board"card"
defaults.releasesbooleanfalse
defaults.weeklyNotesbooleanfalse
defaults.escalateAfterstring—
defaults.quietHoursobject—section
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.workloadobject—section
rules[].when.workload.namesstring[]—up to 64 entries
rules[].when.workload.criticalitycritical | standard—
rules[].when.workload.hotPathtrue—
rules[].when.complexityAtLeasttrivial | low | medium | high—
rules[].when.complexityBelowtrivial | low | medium | high—
rules[].when.attentionstring[]—at least one entry
rules[].when.riskAtLeastinteger—0–100
rules[].when.riskBelowinteger—0–100
rules[].when.sizeAtLeastinteger—0–9007199254740991
rules[].when.sizeBelowinteger—1–9007199254740991
rules[].when.titleMatchesstring—
rules[].when.checksgreen | red | pending | none[]—at least one entry
rules[].routeobjectrequiredsection
rules[].route.channelstring—matches ^(#[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 | readyAndChecksGreen—
rules[].route.digesthourly | daily | weekly—
rules[].route.escalateAfterstring—
rules[].route.threadRepliesboolean—
rules[].route.aiBriefboolean—
rules[].route.notifyAuthorOnReadyboolean—
rules[].route.densitycard | compact | board—
rules[].route.releasesboolean—
rules[].route.weeklyNotesboolean—
rules[].continuebooleanfalse

reminders

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

ai

keytypedefaultnotes
aiobject—section
ai.enabledbooleantrue
ai.providerworkers-ai | openai | anthropic"workers-ai"
ai.modelstring—
ai.briefbooleantrue
ai.reviewerSuggestionsbooleantrue
ai.suggestAttentionbooleantrue
ai.riskAdjustmentboolean—
ai.pulseReportbooleanfalse
ai.maxDiffBytesinteger1800001000–2000000

digests

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

agents

keytypedefaultnotes
agentsobject—section
agents.enabledbooleantrue
agents.extraLoginsstring[][]
agents.minComplexityToNotifytrivial | low | medium | high—Retired: ignored with a migration notice. Use explicit workload or path rules.
agents.minRiskToNotifyinteger—Retired: ignored with a migration notice. Use explicit workload or path rules. · 0–100
agents.holdUntilnone | checksGreen | checksComplete | readyForReview | readyAndChecksGreen"readyAndChecksGreen"
agents.rollupbooleantrue

attention

keytypedefaultnotes
attentionobject—section
attention.pathsobject{}

map

keytypedefaultnotes
mapobject—section
map.enabledbooleantrue
map.publishslack | slack-and-github—
map.animationboolean—
map.componentsobject{}

system

keytypedefaultnotes
systemobject—section
system.workloadsobject[]requiredup to 64 entries
system.workloads[].namestringrequired
system.workloads[].pathsstring[]requiredup to 16 entries
system.workloads[].criticalitycritical | standard | unknown"unknown"
system.workloads[].hotPathboolean—
system.workloads[].intentstring—

githubComment

keytypedefaultnotes
githubCommentboolean—

ignoreRepos

keytypedefaultnotes
ignoreReposstring[][]

ignorePaths

keytypedefaultnotes
ignorePathsstring[][]