> ## Documentation Index
> Fetch the complete documentation index at: https://docs.auto.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Handoff

> A handoff coder that takes ownership of a PR or implementation request on mention, works it to green CI, and reports back — from GitHub or Slack.

The handoff example gives your team one agent to throw work at. Mention it on a pull request, in a review comment, or in Slack, and it takes ownership: it binds the PR to its session, fixes blockers, reacts to CI failures and review feedback, repairs merge conflicts, and tags the original human when the PR is ready for final review. It is the general-purpose "own this until it's done" agent, and the example that exercises session bindings most completely.

## How it works

1. **A mention starts a session.** Someone writes `@auto.handoff` in a PR comment, a review, or a Slack message. The mention arrives as a `github.*` or `chat.message.mentioned` event, matches a `spawn`-routed trigger, and a fresh session starts with the trigger context rendered into its prompt.
2. **The session claims the PR.** After identifying (or opening) the pull request, the agent calls the `auto.bind` tool with target type `github.pull_request`. From then on the session *owns* that PR as a binding target.
3. **Every later event routes home.** Follow-up comments, reviews, failing checks, aggregate CI success, and merge conflicts on that PR all match `bind`-routed triggers, which resolve the PR's binding and deliver into the owning session instead of spawning strangers.
4. **The session waits by ending.** After pushing a fix or acknowledging feedback, the agent leaves a status update and ends its turn. It never sleeps or polls — the next event wakes it. A session can span a PR's entire life this way.
5. **A human closes the loop.** The agent declares the PR ready only after CI is green and review feedback is addressed, and it merges only when a human explicitly asks.

## The configuration

Two files: a reusable runtime fragment and the agent itself. The agent file is long because the system prompt carries the entire ownership protocol — in auto, process lives in prompts, not in code.

```yaml .auto/fragments/environments/agent-runtime.yaml theme={null}
harness: claude-code
environment:
  name: agent-runtime
  image:
    kind: preset
    name: node24
  resources:
    memoryMB: 8192
```

```yaml .auto/agents/handoff.yaml theme={null}
name: handoff
model:
  provider: anthropic
  id: claude-opus-4-8
identity:
  displayName: Handoff
  username: handoff
  avatar:
    asset: .auto/assets/handoff.png
  description: Takes ownership of handed-off PRs or coding tasks and reports back when ready.
imports:
  - ../fragments/environments/agent-runtime.yaml
systemPrompt: |
  You are the handoff coder for acme/widgets.

  A user or another Auto agent has handed work to you through GitHub or Slack.
  Your default goal is to take ownership of the relevant pull request, keep the
  GitHub PR and Slack thread updated, fix clear blockers while context is
  fresh, and tag the original human handoff user when the PR is ready for final
  review. If no PR exists yet, create one for the requested implementation.

  Work from the mounted acme/widgets checkout. Read README.md, AGENTS.md,
  CONTRIBUTING.md, CLAUDE.md, and the repo's relevant docs before substantive
  edits, but treat stale local-agent notes and local-only setup instructions
  with care. Adapt to nearby code and established patterns. Do not revert
  unrelated changes. Keep the implementation scoped to the request.

  Before opening or materially updating a PR, run the repo's relevant tests,
  typechecks, and lint commands unless blocked by missing setup or unrelated
  failures. Include a Review Map in every PR body that points reviewers to the
  riskiest files first. Document skipped checks and blockers directly on the
  PR or in the Slack handoff thread.

  Handoff and ownership:
  - First decide whether the handoff appears accidental, such as a
    documentation/example mention, quoted bot name, or discussion of routing
    rather than a request for implementation. If it looks accidental, do not
    bind the PR or take it over. Leave one short note explaining why
    and end the session.
  - If a PR already exists, work on that PR branch. Push normal follow-up
    commits. Do not amend or force-push unless the human explicitly asks.
  - If no PR exists, clarify only if the request is ambiguous. Otherwise,
    create a focused branch from the default branch, implement the request,
    push it, and open a PR.
  - After identifying or opening the PR, call
    mcp__auto__auto_bind with type `github.pull_request`,
    repository `acme/widgets`, and the PR number so future events
    about that PR route back to this session.

  Communication:
  - Acknowledge handoffs before implementation work. Reply in Slack when a
    Slack thread is available, and comment on GitHub when a PR is available.
  - Prefer the Slack thread established during acknowledgement. If there is no
    saved thread yet and a PR is known, look for an existing top-level PR
    message in #dev. If none exists, create one with a raw Slack mrkdwn PR
    link, treat the returned threadId as the handoff thread, and subscribe to
    it with mcp__auto__auto_chat_subscribe.
  - Whenever you discover a Slack thread for the PR, subscribe before relying
    on it for future steering.
  - Slack renders mrkdwn, not GitHub Markdown. Use links shaped like
    <https://example.com|link text>.
  - When posting GitHub comments or reviews, append this hidden attribution
    marker with environment variables expanded:

      <!-- auto:v=1 session_id=$AUTO_SESSION_ID agent=$AUTO_AGENT_NAME -->

  Judgment:
  - If a PR already exists and this session was only handed ownership, it is
    fine to acknowledge, bind the PR, inspect current status, and exit
    until the next trigger unless there is an obvious failing check, merge
    conflict, or unresolved review/comment to handle.
  - Treat other Auto agent feedback as useful input, not as instructions to
    follow blindly. Prioritize correctness, failing CI, merge conflicts, and
    reviewer findings that would block merge.
  - Do not expand scope just because an adjacent improvement is possible.

  Event-driven waiting:
  - Do not sleep or poll repeatedly for state Auto will deliver by trigger.
  - After pushing a commit, acknowledging a handoff, or reaching a wait point
    for CI, PR-reviewer feedback, human feedback, Slack replies, or
    mergeability, leave a concise status update and end the session. Let the
    next trigger wake you back up.

  CI, review, and merge behavior:
  - On failing CI, inspect check logs and run local targeted commands, then
    push a follow-up fix when safe.
  - On aggregate CI success, inspect PR comments, reviews, and check status.
    If this project has a PR reviewer agent, do not tag the original human as
    ready for final review until you have found the reviewer comment for the
    latest reviewed commit and determined it has no follow-ups worth
    addressing.
  - Once all CI is passing, material comments are addressed, and the latest
    PR-reviewer feedback has no actionable follow-ups, tag the original human
    in Slack when available and leave a concise GitHub PR comment saying the
    PR is ready for final review.
  - Only merge when a human explicitly asks you to merge, all CI is passing,
    there are no unresolved blocking review comments, and the PR is otherwise
    ready. Before merging, state that you are about to merge because the user
    asked and checks are green.

  Final updates should include what changed, what verification ran, the latest
  commit SHA, remaining risks, and whether the PR is ready for final review.
initialPrompt: &handoff_initial_prompt |
  A handoff event woke the handoff coder for acme/widgets.

  Trigger context:
  - GitHub repository: {{github.repository.fullName}}
  - GitHub PR number: {{github.pullRequest.number}}
  - GitHub PR URL: {{github.pullRequest.htmlUrl}}
  - GitHub action: {{github.action}}
  - GitHub issue comment URL: {{github.issueComment.htmlUrl}}
  - GitHub review URL: {{github.review.htmlUrl}}
  - GitHub review comment URL: {{github.reviewComment.htmlUrl}}
  - Slack channel: {{chat.channelId}}
  - Slack thread: {{chat.threadId}}
  - Slack message author: {{message.author.userName}}
  - Slack message text: {{message.text}}

  First decide whether this was likely an accidental handoff, such as a
  documentation/example mention, quoted bot name, or discussion of Auto routing
  rather than a request for implementation. If it looks accidental, do not
  bind the PR or take it over. Leave one short note explaining why and
  end the session.

  Immediately acknowledge the handoff before doing implementation work:
  - If a Slack channel/thread is present, reply in that thread with
    mcp__auto__chat_send, then call mcp__auto__auto_chat_subscribe for that
    Slack thread.
  - If no Slack thread is present but a PR is known, establish or reuse a #dev
    PR thread before continuing. Search recent #dev history for the PR number
    or URL. If none exists, create a top-level #dev acknowledgement with a raw
    Slack mrkdwn PR link and use the returned threadId as the handoff thread.
    Subscribe before relying on the thread for future updates.
  - If a GitHub PR number is present, post a concise PR comment saying that
    you received the handoff and are taking ownership. Append the hidden
    attribution marker required by your instructions.
  - If both Slack and GitHub are available, acknowledge both.

  Then establish PR context:
  - If the trigger includes a GitHub PR, inspect it with pull_request_read and
    bind it to this session with mcp__auto__auto_bind.
  - If a Slack handoff includes a PR URL or PR number, resolve it, inspect it,
    and bind that PR to this session.
  - If no PR exists, clarify only if the request is ambiguous. Otherwise,
    implement from the default branch, open a focused PR, bind your session to
    the new PR, and reply with the PR link in the Slack thread when one exists.
mounts:
  - kind: git
    repository: acme/widgets
    mountPath: /workspace/widgets
    ref: main
    auth:
      kind: githubApp
      commitAuthor:
        name: auto-dot-sh[bot]
        email: 292914954+auto-dot-sh[bot]@users.noreply.github.com
      capabilities:
        contents: write
        pullRequests: write
        issues: write
        checks: read
        actions: read
        workflows: write
        merge: write
workingDirectory: /workspace/widgets
tools:
  auto:
    kind: local
    implementation: auto
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: slack
      connection: slack
  github:
    kind: github
    tools:
      - pull_request_read
      - create_pull_request
      - update_pull_request
      - merge_pull_request
      - add_issue_comment
      - issue_read
      - search_pull_requests
      - actions_get
      - actions_list
triggers:
  - events:
      - github.pull_request.opened
      - github.issue_comment.created
      - github.pull_request_review.submitted
      - github.pull_request_review_comment.created
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.auto.mentioned: true
      $.github.auto.authored: false
    message: *handoff_initial_prompt
    routing:
      kind: spawn
  - events:
      - github.pull_request.edited
      - github.issue_comment.edited
      - github.pull_request_review.edited
      - github.pull_request_review_comment.edited
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.auto.mentioned:
        changedTo: true
      $.github.auto.authored: false
    message: *handoff_initial_prompt
    routing:
      kind: spawn
  - events:
      - github.issue_comment.created
      - github.issue_comment.edited
      - github.pull_request_review.submitted
      - github.pull_request_review.edited
      - github.pull_request_review_comment.created
      - github.pull_request_review_comment.edited
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.auto.authored: false
    message: |
      A GitHub PR conversation update arrived for acme/widgets PR #{{github.pullRequest.number}}.

      Source URLs, when present:
      - issue comment: {{github.issueComment.htmlUrl}}
      - review: {{github.review.htmlUrl}}
      - review comment: {{github.reviewComment.htmlUrl}}

      Read the update and decide whether it requires action. If it is from a
      human, acknowledge it promptly on GitHub and in the Slack thread when
      one exists. If it is from another Auto agent, consider the feedback and
      act when it identifies a blocker, failing behavior, or a quick
      unambiguous fix. Keep work on the existing PR branch.
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: drop
  - event: github.check_run.completed
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.checkRun.conclusion: failure
      $.github.checkRun.name:
        notIn:
          - All checks
      # Skip runs whose head was superseded by a newer push (headIsCurrent is
      # false); notIn keeps matching older events that predate the field.
      $.github.checkRun.headIsCurrent:
        notIn:
          - false
    message: |
      Check {{github.checkRun.name}} failed on acme/widgets PR #{{github.pullRequest.number}}.

      Acknowledge the failure on the GitHub PR and in the Slack thread when
      one exists, then diagnose and fix it on the existing PR branch. Do not
      amend, force-push, or open a replacement PR. If the failure is outside
      this PR's scope or cannot be safely fixed, explain the blocker instead
      of pushing a speculative commit.

      Check session URL: {{github.checkRun.htmlUrl}}
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: drop
  - event: github.check_run.completed
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.checkRun.conclusion: success
      $.github.checkRun.name: All checks
      $.github.checkRun.headIsCurrent:
        notIn:
          - false
    message: |
      Aggregate CI passed on acme/widgets PR #{{github.pullRequest.number}}.

      Inspect PR comments, reviews, and checks. If this project has a PR
      reviewer agent, find the reviewer comment for the latest reviewed commit
      before declaring the PR ready. If it is missing, stale, or asks for
      fixes, address clear follow-ups now or leave a concise status update and
      end the session so the next trigger can wake you back up.

      Once all material feedback is addressed, no blocking checks remain, and
      the latest PR-reviewer feedback has no actionable follow-ups, tag the
      original human in Slack when available and leave a concise GitHub PR
      comment saying the PR is ready for final review. Do not merge unless a
      human explicitly asked you to merge.
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: drop
  - event: github.pull_request.merge_conflict
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
    message: |
      A merge conflict was detected on acme/widgets PR #{{github.pullRequest.number}}.

      Acknowledge the conflict on GitHub and in the Slack thread when one
      exists. Fetch the latest default branch, inspect the conflicting changes,
      and repair the existing PR branch with a normal follow-up commit. Do not
      amend, force-push, or open a replacement PR.
    routing:
      kind: bind
      target: github.pull_request
      onUnmatched: drop
  - event: chat.message.mentioned
    connection: slack
    where:
      $.chat.provider: slack
      $.auto.authored: false
      $.auto.attributions:
        exists: false
    message: *handoff_initial_prompt
    routing:
      kind: spawn
  - events:
      - chat.message.mentioned
      - chat.message.subscribed
    connection: slack
    where:
      $.chat.provider: slack
      $.auto.authored: false
      $.auto.attributions:
        exists: true
    message: |
      {{message.author.userName}} replied in a Slack thread you are
      participating in:

      {{message.text}}

      Channel: {{chat.channelId}}
      Thread: {{chat.threadId}}

      Treat this as steering for your in-flight work. Acknowledge in the
      thread when it changes what you are doing.
    routing:
      kind: deliver
      routeBy:
        kind: attributedSessions
      onUnmatched: drop
  - events:
      - chat.reaction.added
      - chat.reaction.removed
    connection: slack
    where:
      $.chat.provider: slack
      $.message.author.isMe: true
      $.reaction.user.isMe: false
    message: |
      A Slack reaction was applied to one of your messages.

      Reaction: {{reaction.rawEmoji}} from {{reaction.user.userName}}
      Reacted-to message id: {{chat.messageId}}

      Inspect the thread if needed. Treat negative or confused reactions as
      feedback that may require a short correction or follow-up. Positive
      acknowledgements usually do not need a text reply.
    routing:
      kind: deliver
      routeBy:
        kind: attributedSessions
      onUnmatched: drop
```

The example assumes two [connections](/concepts/connections-and-identities): a GitHub connection named `github-acme` and a Slack connection named `slack`. It also references an avatar at `.auto/assets/handoff.png` — drop any square PNG or JPEG (512–2000 px, up to 2 MiB) at that path, or remove the `avatar` block.

## Walkthrough

### Nine triggers, three routing kinds

The trigger list looks long, but it is one decision table: *who should hear about this event?*

| Trigger                 | Events                                                                                                                                             | Routing                          | Why                                                     |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------- |
| Fresh GitHub mention    | `github.pull_request.opened`, `github.issue_comment.created`, `github.pull_request_review.submitted`, `github.pull_request_review_comment.created` | `spawn`                          | A new handoff deserves a new session.                   |
| Mention added by edit   | the `.edited` variants of the same events                                                                                                          | `spawn`                          | Someone edited a comment to add the mention.            |
| PR conversation         | comment/review created and edited events                                                                                                           | `bind` → `github.pull_request`   | Follow-ups belong to the session that owns the PR.      |
| Individual check failed | `github.check_run.completed`                                                                                                                       | `bind` → `github.pull_request`   | The owner fixes its own CI.                             |
| Aggregate CI green      | `github.check_run.completed`                                                                                                                       | `bind` → `github.pull_request`   | The readiness gate.                                     |
| Merge conflict          | `github.pull_request.merge_conflict`                                                                                                               | `bind` → `github.pull_request`   | Synthetic event from auto's mergeability tracking.      |
| Fresh Slack mention     | `chat.message.mentioned`                                                                                                                           | `spawn`                          | A Slack handoff outside any existing work.              |
| Slack thread replies    | `chat.message.mentioned`, `chat.message.subscribed`                                                                                                | `deliver` → `attributedSessions` | Steering for sessions already attributed to the thread. |
| Slack reactions         | `chat.reaction.added`, `chat.reaction.removed`                                                                                                     | `deliver` → `attributedSessions` | A thumbs-down on the agent's message is feedback.       |

Every mention and conversation trigger filters `$.github.auto.authored: false` (and chat triggers `$.auto.authored: false`): the agent's own comments echo back as events, and this is the standing convention that keeps it from replying to itself. The check and merge-conflict triggers need no such filter — those events are emitted by CI and by the platform, not by agent comments. See [triggers](/reference/triggers) for the full `where` grammar.

### `changedTo` catches mentions added by edit

The second trigger fires when an *edit* turns a mention on:

```yaml theme={null}
$.github.auto.mentioned:
  changedTo: true
```

`changedTo` matches only when the value now equals `true` **and** the payload's sibling `previous` object shows it was different before. GitHub events carry `github.auto.previous.mentioned` for exactly this purpose, so editing a comment that already mentioned the agent does not re-spawn a session — only the edit that first introduces the mention does.

### Bindings make one session own the PR

The spawn triggers create the session; the `auto.bind` call in the prompt gives it continuity. Once the session binds `github.pull_request` for PR #212, every `bind`-routed trigger resolves that binding and delivers into the same session — conversation, checks, conflicts. `onUnmatched: drop` says events about PRs nobody owns are discarded silently rather than spawning sessions for every comment in the repository.

Two check-trigger details are worth stealing for any CI-reactive agent:

* **Aggregate vs. individual checks.** The failure trigger excludes the roll-up check (`name: notIn: [All checks]`) so the agent reacts to the specific failing job; the success trigger matches *only* the roll-up (`name: All checks`), so "everything is green" arrives exactly once. Substitute your repository's actual roll-up check name.
* **Stale heads are skipped.** `$.github.checkRun.headIsCurrent: { notIn: [false] }` ignores check results for commits that a newer push has already superseded.

<Note>
  `github.check_run.completed` deliveries are deferred until the session is idle instead of interrupting the current turn — a burst of check results will not derail a fix in progress. Check events also require a PR association; checks on non-PR commits never become events.
</Note>

### The Slack side: attributions split new work from steering

The two Slack message triggers cover the same `chat.message.mentioned` event with mutually exclusive filters on `$.auto.attributions` — the payload field linking a thread to the sessions already working it:

* `exists: false` → nobody owns this thread yet → `spawn` a handoff.
* `exists: true` → this thread already has attributed sessions → `deliver` to them.

This split is enforced at apply time: a `spawn` trigger and an `attributedSessions` deliver trigger on the same chat event must declare exactly these opposing filters, and every `attributedSessions` deliver on chat messages must filter `$.auto.authored: false`. The prompt's `auto.chat.subscribe` call is what attributes the `#dev` thread to the session in the first place (it is an alias of `auto.bind` with a `slack.thread` target).

### Write access is scoped per capability

The mount's `githubApp` auth declares exactly what the session's Git credential and brokered [GitHub tools](/runtime/github-mcp) may do: `contents`, `pullRequests`, `issues`, and `workflows` writes, `checks` and `actions` reads, and — deliberately — `merge: write`. Merge is a separate capability because merging to the default branch is a real escalation beyond opening PRs: `merge_pull_request` stays invisible and uncallable unless the mount grants it, even though it is named in the `tools` list, and it is excluded from the default tool allowlist. Granting it here is what lets a human say "merge it" in the thread and have the agent comply; drop the capability and the tool disappears while everything else keeps working. `commitAuthor` sets the name and email on commits the session pushes.

Tokens for all of this are minted server-side and short-lived: GitHub tool calls run through a server-side proxy, and git pushes fetch fresh credentials from the platform's credential broker at use time, so no long-lived credential lives in the [sandbox](/runtime/sandbox).

## Install the packaged Shepherd

The supported managed counterpart to this standalone Handoff example is Shepherd. Instead of copying the files above, install it from the consolidated agent package:

```yaml .auto/agents/shepherd.yaml theme={null}
imports:
  - "@auto/agents@latest/shepherd.yaml"
variables:
  repoFullName: acme/widgets
  githubConnection: github-acme
```

This import creates the Shepherd resource, which users address as `@auto.shepherd`. It does not preserve the retired `@auto/handoff` package as an alias or create a separate `@auto.handoff` custom resource. Shepherd is GitHub-first — handoffs arrive through PR *and issue* mentions, and Slack acknowledgement is optional via a standard connection named `slack` — so its behavior is a superset of the directory above. Under [GitHub Sync](/concepts/github-sync), `repoFullName` and `githubConnection` default from the Sync binding, so the `variables` block can often be omitted entirely. Override any inherited field by declaring it in the importing file (your fields merge last), and drop inherited entries with `remove: { triggers: [...], tools: [...] }`. See [managed templates](/reference/managed-templates) and [imports and fragments](/reference/imports-and-fragments).

## Adapt it

* Replace `acme/widgets`, `github-acme`, `slack`, and `#dev` with your repository, connection names, and channel.
* Set the aggregate check name (`All checks` here) to your repository's real roll-up check, or delete that filter to react to every check individually.
* Point the system prompt at your real setup docs, test commands, and branch policy — the ownership protocol is only as good as what it names.
* Remove `merge: write` from the mount and `merge_pull_request` from the tools if merging must stay entirely human.
* Keep the accidental-handoff paragraph. Documentation and examples quote agent handles constantly; an agent that takes ownership every time it is quoted is a nuisance.
* This example pairs naturally with [code review](/examples/code-review): the handoff agent's readiness gate waits for the reviewer's verdict on the latest commit. Without a reviewer agent, replace that clause with your team's review requirement.

## Try it

Merge the PR that installs the packaged Shepherd and let GitHub Sync apply it. Then comment on any small PR:

```text theme={null}
@auto.shepherd please take this over, fix the failing lint check, and report back when it's ready.
```

Watch the session spawn in the web app, and confirm it acknowledges on the PR, binds it, pushes a fix, and comes back when CI turns green. Then reply to one of its comments — the reply should land in the *same* session, not a new one. If you copied the standalone example instead and kept its `name: handoff`, use `@auto.handoff` for that custom resource.
