Reactions
Reactions are feedback loops that respond to events on a Sortie-created pull request after the initial agent run hands off. Each reaction kind watches one external signal (failing CI, requested review changes, or a mergeable approved PR) and either dispatches a continuation turn so the agent can respond, or, for auto-merge, performs the merge directly. Reactions are opt-in: a kind is inactive until its provider is set, and omitting the reactions block disables all of them.
See also: workflow configuration reference for the reactions block and the tracker.handoff_state and tracker.active_states fields; state machine reference for claims, retries, and the reconcile tick that drives reaction processing; GitHub adapter reference for the SCM provider and token; how to configure CI feedback and how to configure PR review feedback for setup procedures.
Reaction lifecycle
Every reaction kind moves through the same pipeline. The orchestrator records a pending reaction for an issue when a worker exits normally and SCM metadata is available, and it reconstructs eligible pending reactions at startup so feedback survives a restart. On each reconcile tick, after tracker-state refresh, the orchestrator runs the pipeline for each pending reaction in a fixed order: CI failure first, then review comments, then auto-merge.
- Poll. The orchestrator queries the kind’s provider for the current signal, throttled by the kind’s
poll_interval_ms. A transient fetch error re-enqueues the entry for the next tick. - Fingerprint.
review_commentsandauto_mergehash their salient state into a SHA-256 fingerprint stored in thereaction_fingerprintsSQLite table. The review fingerprint is the sorted set of non-outdated comment IDs; the merge fingerprint is the PR head SHA combined with the review decision.ci_failurecomputes no fingerprint and reads CI status directly. - Deduplicate. When the fingerprint matches the last value already marked dispatched, the tick takes no action. A new push or a changed comment set produces a new fingerprint and clears the dispatched mark.
ci_failurededuplicates through status instead: apendingorpassingconclusion takes no action, and a laterpassingresult clears the issue’s attempt counter. - Dispatch. The reaction action runs. For
ci_failureandreview_commentsthe orchestrator cancels any existing continuation retry and schedules a fix continuation turn, injecting the signal into the prompt through a continuation context variable. Forauto_mergethe orchestrator callsMergePRdirectly, since no code change is needed. Each dispatch increments the per-issue, per-kind attempt counter and uses a fixed 1-second delay rather than exponential backoff. - Escalate. When the attempt counter reaches the kind’s retry budget, the orchestrator applies the configured
escalationaction, releases the claim, and clears that kind’s pending state.
flowchart TD
EX[Normal worker exit] --> PE[Pending reaction recorded]
PE --> PL{Poll provider}
PL -->|signal not actionable| PL
PL -->|actionable| FP{Fingerprint changed?}
FP -->|no| PL
FP -->|yes| BUD{Within retry budget?}
BUD -->|yes| DI[Dispatch: continuation turn or merge]
DI --> PL
BUD -->|no| ES([Escalate and release claim])
classDef start fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
classDef decision fill:#fef3c7,stroke:#d97706,color:#78350f
classDef action fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px
classDef terminal fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
class EX,PE start
class PL,FP,BUD decision
class DI action
class ES terminalRetry budgets
The attempt counter is tracked per issue and per kind. It resets when the issue leaves the running and retry maps, and ci_failure also resets it when CI returns to passing. The budget field differs by kind: ci_failure and auto_merge use max_retries (default 2), and review_comments uses max_continuation_turns (default 3) as its hard cap. A budget of 0 escalates on the first actionable signal with no fix attempt.
State eligibility
Reaction continuations dispatch even while the issue sits in the tracker’s handoff_state, the state Sortie transitions to for human review after a successful run. This differs from fresh-work retries (stall recovery and transient agent errors), which dispatch only when the issue is in an active_state. An issue that has moved to any other state, including a terminal state, releases its claim on the next retry and runs no further reactions. See the state machine reference for the claim and retry model.
Cross-kind isolation
Each kind owns its own pending entry, fingerprint row, and attempt counter. A successful auto-merge, or escalation of any one kind, scopes its cleanup to that kind alone and leaves the other kinds’ state on the same issue intact.
Escalation actions
When a kind exhausts its budget, the orchestrator applies one escalation action and releases the claim:
label(default): addsescalation_label(defaultneeds-human) to the tracker issue.comment: posts a plain-text tracker comment naming the PR, the attempt count, and the outstanding signal.
The action runs in a detached goroutine with a 30-second timeout. A failed escalation is logged and counted but does not block claim release. CI escalation outcomes are recorded by the sortie_ci_escalations_total counter; see the Prometheus metrics reference.
Common fields
Every reaction kind shares these four fields.
| Field | Type | Default | Description |
|---|---|---|---|
provider | string | (required) | SCM or CI adapter kind that activates the reaction (e.g. github). Must match a registered adapter. Absent or empty disables the kind, and all other fields in the sub-object are ignored. |
max_retries | integer | 2 | Fix continuation dispatches per issue before escalation. Must be non-negative. |
escalation | string | label | Action on budget exhaustion. One of label or comment. |
escalation_label | string | needs-human | Label applied to the tracker issue when escalation is label. |
Keys other than these four are kind-specific and listed under each kind below.
Note
Environment variable overrides for reactions fields are not supported. Reaction configuration comes from WORKFLOW.md. The provider value takes effect at startup; the remaining fields take effect on the next dispatch after a dynamic reload.
Reaction kinds
reactions.ci_failure
Polls CI status for Sortie-created branches and dispatches a continuation turn when CI fails. This kind supersedes the deprecated top-level ci_feedback block; when both are present, reactions.ci_failure takes precedence and a deprecation warning is logged.
Fields (beyond the common fields):
| Field | Type | Default | Description |
|---|---|---|---|
max_log_lines | integer | 50 | Maximum CI log tail lines injected into the prompt. 0 disables log injection. |
Activation: active when provider names a registered CI status provider. The orchestrator reads the CI ref from .sortie/scm.json (SHA preferred, branch as fallback).
Behavior: the reconcile loop fetches CI status each tick. A pending or passing status re-enqueues with no dispatch; a passing status also clears the attempt counter. A failing status increments the attempt counter and, while within max_retries, dispatches a continuation turn carrying the failing checks through the .ci_failure template variable. See the .ci_failure template variable for its schema.
Example:
reactions:
ci_failure:
provider: github
max_retries: 2
max_log_lines: 50
escalation: label
escalation_label: needs-humanreactions.review_comments
Polls human CHANGES_REQUESTED review comments on Sortie-created PRs and dispatches a continuation turn so the agent can address the feedback. Bot and automated comments are filtered out by author type. This kind reads review state only; it does not create PRs, approve reviews, or resolve comments.
Fields (beyond the common fields):
| Field | Type | Default | Description |
|---|---|---|---|
poll_interval_ms | integer | 120000 | Minimum interval between review API polls per issue. Minimum: 30000. |
debounce_ms | integer | 60000 | Wait after the newest detected comment before dispatching. Must be non-negative. |
max_continuation_turns | integer | 3 | Hard cap on review-triggered continuations per PR. Must be positive. |
Activation: active when provider names a registered SCM adapter. The agent or an after_run hook must write pr_number (positive integer), owner, and repo to .sortie/scm.json in the workspace. When any field is missing or zero, review polling is skipped for that workspace with no error.
Behavior: comments newer than debounce_ms defer dispatch until the reviewer’s batch settles. The fingerprint is the SHA-256 of the sorted non-outdated comment IDs; a changed comment set triggers a new continuation, and an unchanged set is skipped. Dispatch injects the comments through the .review_comments template variable (a list of maps with keys id, file, start_line, end_line, reviewer, body). Escalation fires when the attempt counter reaches max_continuation_turns. See the .review_comments template variable for its schema.
Example:
reactions:
review_comments:
provider: github
max_retries: 2
escalation: label
escalation_label: needs-human
poll_interval_ms: 120000
debounce_ms: 60000
max_continuation_turns: 3reactions.auto_merge
Polls merge preconditions on Sortie-created PRs and merges directly through the SCM adapter once they hold. Auto-merge is off by default and activates only when provider is set. There is no separate enabled flag; the presence of provider is the activation key, matching the other reaction kinds. The runtime and persisted kind value for this reaction is merge, not auto_merge.
Fields (beyond the common fields):
| Field | Type | Default | Description |
|---|---|---|---|
strategy | string | squash | Merge strategy. One of merge, squash, or rebase. |
require_ci | boolean | true | When true, every CI check must pass before the merge. When false, CI is advisory only. |
delete_branch | boolean | true | When true, the PR head branch is deleted after a successful merge. A delete failure does not roll back the merge. |
poll_interval_ms | integer | 60000 | Minimum interval between precondition checks per issue. Minimum: 30000. |
Activation: active when provider names a registered SCM adapter. The agent or an after_run hook must write pr_number, owner, repo, and branch (all non-empty) to .sortie/scm.json. When reactions.review_comments is also configured, both kinds must name the same provider; a mismatch or an unknown provider fails startup. At startup the orchestrator runs a one-shot token-scope preflight: the merge endpoint needs pull_requests:write, branch deletion needs contents:write, and the classic repo scope covers both. An auth-class scope failure disables auto-merge for the process lifetime; a transport-class failure schedules one retry on the next tick before disabling.
Merge preconditions: the orchestrator merges only when all of the following hold. While any is unmet, the entry re-enqueues at the poll interval.
| Precondition | Requirement for merge |
|---|---|
| Ownership | The PR is Sortie-created, identified by .sortie/scm.json. |
| Draft state | The PR is not a draft. |
| Mergeability | GitHub reports clean or unstable (no conflicts). |
| Review | The review decision is APPROVED, or reviews are not required (NOT_REQUIRED). |
| CI | The CI conclusion is success when require_ci is true; ignored when false. |
Behavior: the merge fingerprint is the SHA-256 of the PR head SHA combined with the review decision, so a new push or a change in review decision allows a fresh attempt. MergePR is called with the expected head SHA to close the time-of-check to time-of-use window between the precondition read and the merge. A 409 response whose body reports the PR is already merged is treated as success. Escalation fires when the attempt counter reaches max_retries, or immediately on an authentication error.
Safety: auto-merge acts on Sortie-created PRs only and never merges a draft. The merge is performed directly rather than through an agent turn.
Warning
A merge is irreversible. Sortie does not roll back on tail-step failures such as branch deletion or the confirmation comment. Auto-merge stays off unless reactions.auto_merge.provider is set, and enabling it is a conscious opt-in.
Example (conservative opt-in):
reactions:
review_comments:
provider: github # SCM provider; must match auto_merge below
auto_merge:
provider: github # activates auto-merge; no separate "enabled" flag
strategy: squash # squash | merge | rebase
require_ci: true # never merge on failing or pending CI
delete_branch: true # remove the head branch after a successful merge
max_retries: 2 # merge attempts before escalation
escalation: comment # post a tracker comment when attempts are exhausted
poll_interval_ms: 60000 # 60s between precondition checksValidation rules
- Reaction kind keys must match
[a-z][a-z0-9_-]*. Invalid keys are rejected with a configuration error. max_retriesmust be non-negative for all kinds.escalationmust belabelorcommentfor all kinds.poll_interval_msmust be at least30000forreview_commentsandauto_merge.debounce_msmust be non-negative, andmax_continuation_turnsmust be positive, forreview_comments.strategyforauto_mergemust bemerge,squash, orrebase.require_cianddelete_branchforauto_mergemust be boolean.- When
reactions.review_commentsandreactions.auto_mergeare both present, they must declare the sameprovider.
sortie validate reports these errors before dispatch. See the CLI reference for the validate subcommand.
Was this page helpful?