CI feedback closes the loop between your CI pipeline and Sortie's agents. When a CI pipeline fails on a branch that an agent pushed, Sortie detects the failure, injects failure context into the agent's prompt, and dispatches a continuation run so the agent can fix the problem. If the agent can't fix it after repeated attempts, Sortie escalates to a human. This guide walks you through activating CI feedback, tuning its behavior, and verifying it works.

Prerequisites

  • Sortie running with the GitHub tracker adapter (tracker.kind: github) — see Connect to GitHub
  • A branch-per-issue hook workflow that pushes commits — see Setup workspace hooks
  • CI configured on the repository (GitHub Actions, or any system that reports via the Checks API)
  • A GitHub personal access token with repo scope (needed for the Checks API)

Activate CI feedback

CI feedback is disabled by default. Add a ci_feedback block with a kind field to your WORKFLOW.md front matter to activate it:

ci_feedback:
  kind: github

There is no enabled flag. Presence of kind activates the feature; absence disables it. This follows the same pattern as other Sortie features (e.g., server.port absent means the HTTP server is disabled).

Once activated, Sortie hooks into the worker exit path. After each normal worker exit where the agent pushed code, the orchestrator reads .sortie/scm.json from the workspace to discover the branch and commit SHA. On the next reconcile tick, it polls CI status on that ref. Three outcomes are possible:

  • Passing. CI is green. No action taken. The CI-fix attempt counter resets to zero.
  • Pending. Checks are still running. Sortie re-checks on the next tick.
  • Failing. At least one check failed. Sortie dispatches a continuation run with failure context injected into the prompt.

If you don't see CI feedback triggering, check that your after_run hook writes .sortie/scm.json. Without it, Sortie has no ref to poll and skips the feature silently for that run.

Configure retry limits

ci_feedback:
  kind: github
  max_retries: 2  # default 2

max_retries controls how many CI-fix continuation dispatches Sortie attempts per issue before escalating. Default: 2. Set to 0 to escalate on the first CI failure without retrying.

Each CI failure that triggers a new dispatch increments the counter. If the agent fixes the issue and CI passes, the counter resets to zero. When the counter exceeds max_retries, Sortie escalates via the configured strategy and releases its claim on the issue.

Configure log fetching

ci_feedback:
  kind: github
  max_log_lines: 50  # default 50; 0 = disable

max_log_lines controls how many lines from the first failing check run's log Sortie fetches and includes in the failure context. Default: 50. Set to 0 to disable log fetching.

When log fetching is disabled, the agent still receives structured failure data (which checks failed, their names, statuses, and details URLs). It won't receive the raw log output. Disabling is useful when CI logs contain sensitive data you don't want entering agent prompts, or when you're operating at scale and want to reduce API calls. Each failing check costs one additional API request for log fetching.

Choose an escalation strategy

ci_feedback:
  kind: github
  escalation: label              # "label" (default) or "comment"
  escalation_label: needs-human  # default "needs-human"

When CI-fix retries are exhausted, Sortie escalates. Two strategies are available:

Strategy Behavior
label (default) Adds escalation_label (default needs-human) to the issue. The label must already exist in the repository.
comment Posts a comment on the issue with failure details: how many CI-fix attempts were made, which checks failed, and links to their detail pages.

Both strategies release the claim on the issue and cancel any pending retry. The issue won't be re-dispatched until its tracker state changes.

escalation_label only applies when escalation is label. If you use comment escalation, you don't need this field. Create the label in advance with gh:

gh label create needs-human --repo myorg/myrepo --color "D93F0B"

How Sortie finds the repository and branch

CI feedback needs a repository to query and a ref to check. It gets these from two sources, and you don't need extra config for either.

Repository coordinates come from the tracker adapter. When tracker.kind: github, the tracker block already contains api_key and project (owner/repo). CI feedback reuses these credentials. No additional configuration needed.

Branch and SHA come from .sortie/scm.json in the workspace. Your after_run hook writes this file after pushing code. It contains at minimum a branch field and optionally a sha field:

{"branch": "sortie/PROJ-123", "sha": "abc123def456"}

When both branch and sha are present, Sortie uses the SHA as the ref for more deterministic results. When only branch is present, Sortie queries CI status by branch name.

Here's an after_run hook that pushes and writes the SCM metadata:

git add -A
git diff --cached --quiet || {
  git commit -m "sortie(${SORTIE_ISSUE_IDENTIFIER}): automated changes"
  git push origin "sortie/${SORTIE_ISSUE_IDENTIFIER}" --force-with-lease

  # Write SCM metadata for CI feedback
  SHA=$(git rev-parse HEAD)
  mkdir -p .sortie
  printf '{"branch":"sortie/%s","sha":"%s"}' \
    "${SORTIE_ISSUE_IDENTIFIER}" "${SHA}" > .sortie/scm.json
}

If .sortie/scm.json is absent, has an empty branch field, or is a symlink (rejected for security), CI feedback is skipped for that run.

What the agent sees

On a CI-fix continuation dispatch, Sortie injects failure context into the first-turn prompt via the {{ .ci_failure }} template variable. This variable is nil on normal dispatches and non-CI retries, so your template can conditionally render it.

The ci_failure object contains:

Field Type Description
status string Always "failing" in this context.
check_runs list Individual check runs with name, status, conclusion, details_url.
log_excerpt string Truncated log from the first failing check. Empty when log fetching is disabled.
failing_count integer Number of failing checks.
ref string The git ref (branch or SHA) that was checked.

Add a conditional block to your prompt template:

{{ if .ci_failure }}
## CI Failure

CI is failing on {{ .ci_failure.ref }}.
{{ .ci_failure.failing_count }} check(s) failed.

{{ if .ci_failure.log_excerpt }}
Failure log excerpt:
```
{{ .ci_failure.log_excerpt }}
```
{{ end }}

{{ range .ci_failure.check_runs }}{{ if eq .conclusion "failure" }}
- {{ .name }}: FAILED{{ if .details_url }} ({{ .details_url }}){{ end }}
{{ end }}{{ end }}

Diagnose the failure, fix the code, and push.
Do not modify CI configuration.
{{ end }}

The failure context is injected on the first turn of the CI-fix dispatch only. It persists in the agent's conversation history from turn 1, so subsequent turns within the same session don't need it repeated.

For more on template syntax, see Write a prompt template.

Interaction with existing retry logic

CI-fix dispatches are distinct from error retries and continuation retries. They use a separate counter and apply independently.

Trigger Delay Counter Backoff
Agent error (crash, timeout) Exponential backoff agent.max_sessions agent.max_retry_backoff_ms
Agent success, issue still active 1 second agent.max_sessions None
CI failure on pushed branch 1 second ci_feedback.max_retries None

Both ci_feedback.max_retries and agent.max_sessions are evaluated independently. When either limit is exhausted, its corresponding escalation fires. CI-fix dispatches use a fixed 1-second delay, not exponential backoff, because CI failures are a signal to try fixing code, not a sign of transient infrastructure problems.

If the agent signals blocked via .sortie/status during a CI-fix run, the orchestrator respects that signal and releases the claim without further CI checks. For details on the agent-to-orchestrator protocol, see the agent extensions reference.

Complete example

A full WORKFLOW.md with CI feedback, GitHub Issues, branch-per-issue hooks, and a prompt template that renders CI failure context:

---
tracker:
  kind: github
  api_key: $SORTIE_GITHUB_TOKEN
  project: myorg/myrepo
  active_states: [backlog, in-progress]
  terminal_states: [done, wontfix]
  handoff_state: review
  in_progress_state: in-progress
  comments:
    on_dispatch: true
    on_completion: true
    on_failure: true

agent:
  kind: claude-code
  max_turns: 5
  max_sessions: 3
  max_concurrent_agents: 2
  stall_timeout_ms: 300000

ci_feedback:
  kind: github
  max_retries: 2
  max_log_lines: 50
  escalation: label
  escalation_label: needs-human

hooks:
  after_create: |
    git clone --depth 1 "https://${SORTIE_GITHUB_TOKEN}@github.com/myorg/myrepo.git" .
  before_run: |
    git fetch origin main
    git checkout -B "sortie/${SORTIE_ISSUE_IDENTIFIER}" origin/main
  after_run: |
    git add -A
    git diff --cached --quiet || {
      git commit -m "sortie(${SORTIE_ISSUE_IDENTIFIER}): automated changes"
      git push origin "sortie/${SORTIE_ISSUE_IDENTIFIER}" --force-with-lease
      SHA=$(git rev-parse HEAD)
      mkdir -p .sortie

printf '{"branch":"sortie/%s","sha":"%s"}' \
        "${SORTIE_ISSUE_IDENTIFIER}" "${SHA}" > .sortie/scm.json
    }
  timeout_ms: 120000

db_path: .sortie.db
server:
  port: 8642
---

You are a senior engineer working on {{ .issue.identifier }}.

## Task

**{{ .issue.identifier }}**: {{ .issue.title }}

{{ if .issue.description }}
{{ .issue.description }}
{{ end }}

{{ if .ci_failure }}
## CI Failure

CI is failing on branch {{ .ci_failure.ref }}.
{{ .ci_failure.failing_count }} check(s) failed.

{{ if .ci_failure.log_excerpt }}
Failure log excerpt:
```
{{ .ci_failure.log_excerpt }}
```
{{ end }}

{{ range .ci_failure.check_runs }}{{ if eq .conclusion "failure" }}
- {{ .name }}: FAILED{{ if .details_url }} ({{ .details_url }}){{ end }}
{{ end }}{{ end }}

Diagnose the CI failure and fix the code. Do not modify CI workflow files.
{{ end }}

{{ if .run.is_continuation }}
Resuming turn {{ .run.turn_number }}/{{ .run.max_turns }}.
{{ end }}

Disable log fetching for API cost control

Set max_log_lines: 0 to skip log fetching entirely:

ci_feedback:
  kind: github
  max_log_lines: 0

The agent still receives check run names, conclusions, and details URLs. Log fetching requires one additional API call per failing check; disabling it saves those requests. Useful when operating under rate limits or when your CI logs are too verbose to be helpful in a prompt.

Verify CI feedback

Three approaches to confirm everything is wired correctly.

Logs

Search for key messages that trace the CI feedback lifecycle:

# CI status polled and passing
grep "CI passing" sortie.log

# CI failure detected, fix dispatch scheduled
grep "CI failure detected" sortie.log

# CI fix dispatch queued
grep "scheduling CI fix dispatch" sortie.log

# Retries exhausted, escalation triggered
grep "CI fix retries exhausted" sortie.log

Dashboard

When the HTTP server is enabled (server.port in your config), the web dashboard shows entries in Retrying state with a ci_fix trigger label. Run history entries with status ci_failed indicate CI failures that were detected. See the dashboard reference.

Prometheus metrics

Three CI-related metrics are available when the HTTP server is enabled:

Metric Labels Description
sortie_ci_status_checks_total result (passing, pending, failing, error) CI status poll outcomes.
sortie_ci_escalations_total action (label, comment, error) Escalation actions taken.
sortie_retries_total trigger (ci_fix) CI-fix dispatches scheduled.

A healthy CI feedback setup shows sortie_ci_status_checks_total{result="passing"} climbing steadily, with occasional failing bumps that correlate with sortie_retries_total{trigger="ci_fix"} increments. Persistent error results on the status check metric indicate a token or permissions problem. For the full metrics catalog, see Prometheus metrics reference.

Configuration reference

All ci_feedback fields in one place:

Field Type Default Description
kind string (absent) CI provider adapter. Currently "github". Presence activates the feature.
max_retries integer 2 Max CI-fix dispatches per issue before escalation. 0 = escalate immediately.
max_log_lines integer 50 Lines to fetch from the first failing check's log. 0 = disable log fetching.
escalation string "label" Escalation strategy: "label" or "comment".
escalation_label string "needs-human" Label to apply when escalation is "label". Must exist in the repo.

For the full WORKFLOW.md configuration reference including all sections, see workflow config reference.