Skip to content
Reactions

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.

  1. 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.
  2. Fingerprint. review_comments and auto_merge hash their salient state into a SHA-256 fingerprint stored in the reaction_fingerprints SQLite 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_failure computes no fingerprint and reads CI status directly.
  3. 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_failure deduplicates through status instead: a pending or passing conclusion takes no action, and a later passing result clears the issue’s attempt counter.
  4. Dispatch. The reaction action runs. For ci_failure and review_comments the orchestrator cancels any existing continuation retry and schedules a fix continuation turn, injecting the signal into the prompt through a continuation context variable. For auto_merge the orchestrator calls MergePR directly, 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.
  5. Escalate. When the attempt counter reaches the kind’s retry budget, the orchestrator applies the configured escalation action, 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 terminal

Retry 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): adds escalation_label (default needs-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.

FieldTypeDefaultDescription
providerstring(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_retriesinteger2Fix continuation dispatches per issue before escalation. Must be non-negative.
escalationstringlabelAction on budget exhaustion. One of label or comment.
escalation_labelstringneeds-humanLabel 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):

FieldTypeDefaultDescription
max_log_linesinteger50Maximum 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-human

reactions.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):

FieldTypeDefaultDescription
poll_interval_msinteger120000Minimum interval between review API polls per issue. Minimum: 30000.
debounce_msinteger60000Wait after the newest detected comment before dispatching. Must be non-negative.
max_continuation_turnsinteger3Hard 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: 3

reactions.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):

FieldTypeDefaultDescription
strategystringsquashMerge strategy. One of merge, squash, or rebase.
require_cibooleantrueWhen true, every CI check must pass before the merge. When false, CI is advisory only.
delete_branchbooleantrueWhen true, the PR head branch is deleted after a successful merge. A delete failure does not roll back the merge.
poll_interval_msinteger60000Minimum 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.

PreconditionRequirement for merge
OwnershipThe PR is Sortie-created, identified by .sortie/scm.json.
Draft stateThe PR is not a draft.
MergeabilityGitHub reports clean or unstable (no conflicts).
ReviewThe review decision is APPROVED, or reviews are not required (NOT_REQUIRED).
CIThe 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 checks

Validation rules

  • Reaction kind keys must match [a-z][a-z0-9_-]*. Invalid keys are rejected with a configuration error.
  • max_retries must be non-negative for all kinds.
  • escalation must be label or comment for all kinds.
  • poll_interval_ms must be at least 30000 for review_comments and auto_merge.
  • debounce_ms must be non-negative, and max_continuation_turns must be positive, for review_comments.
  • strategy for auto_merge must be merge, squash, or rebase.
  • require_ci and delete_branch for auto_merge must be boolean.
  • When reactions.review_comments and reactions.auto_merge are both present, they must declare the same provider.

sortie validate reports these errors before dispatch. See the CLI reference for the validate subcommand.

Was this page helpful?