> ## 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.

# Issue Triage

> A triage agent that grooms every new issue and hands implementation-ready work to Shepherd, which opens the PR and reports back.

This example runs two cooperating agents. **issue-triage** wakes for every new GitHub issue (and whenever a human re-requests triage with a label): it dedupes, prioritizes, labels, splits mixed reports, and asks for missing context. When an issue is clear enough to implement, it hands off to **shepherd** — the implementation owner that opens a pull request, carries it through review, and comments back on the issue with the PR link, tests run, and residual risks.

Use it when your issue tracker accumulates faster than anyone grooms it, and you want vague reports turned into implementation-ready work — or shipped PRs — without a human dispatcher. GitHub Issues is the base; a Linear variant swaps the source of truth.

## How it works

```mermaid theme={null}
flowchart TD
    A["github.issue.opened"] --> C{"Session bound<br/>to this issue?"}
    B["github.issue.labeled<br/>(auto-triage)"] --> C
    C -->|no| D["Spawn triage session,<br/>bound to the issue"]
    C -->|yes| E["Deliver into the<br/>owning triage session"]
    D --> F["Dedupe · prioritize · label ·<br/>ask for missing context"]
    E --> F
    F -->|"implementation-ready"| G["auto.sessions.spawn<br/>agent: shepherd"]
    G --> H["Shepherd: branch → tests →<br/>PR with Review Map →<br/>comment back on the issue"]
```

| Agent          | Triggers                                                                                    | Routing                                        | Writes                                                                                                                    |
| -------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `issue-triage` | `github.issue.opened`; `github.issue.labeled` filtered to the `auto-triage` label           | `bind` on `github.issue`, `onUnmatched: spawn` | Issue labels, state, and comments (`issues: write`); never code                                                           |
| `shepherd`     | none required for this handoff — it is spawned by triage via the `auto.sessions.spawn` tool | —                                              | A feature branch, one PR carried through review, and one closing issue comment (`contents: write`, `pullRequests: write`) |

Two mechanics carry the example:

* **The issue binding.** Both triage triggers route `bind` on the `github.issue` target with `onUnmatched: spawn`: the first event on an issue spawns a session already bound to it, and a repeat event on an issue whose triage session is still live delivers into that session instead of spawning a duplicate.
* **Agent-to-agent handoff.** Triage calls the [`auto.sessions.spawn` tool](/runtime/auto-tools) with `agent: shepherd` and a message carrying the issue number, URL, triage summary, acceptance criteria, and constraints. Shepherd receives the full brief in the spawn message, so this dispatch path needs no duplicate issue trigger.

The `auto-triage` label is a **one-shot request token**, not a standing subscription: triage removes it once it has acted, so re-applying the label is how humans request another pass.

## Install from the managed template

Issue Triage and Shepherd are separate retained roles in `@auto/agents`:

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

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

For the Linear substrate, import `@auto/agents@latest/issue-triage-linear.yaml`. The catalog installer automatically adds this optional Linear reporting overlay to the separately installed Shepherd; add the same overlay to a hand-authored facade:

```yaml .auto/agents/shepherd.yaml (Linear overlay) theme={null}
systemPrompt:
  append: |

    For a Linear issue dispatched by Issue Triage, use chat.send with target
    provider `linear` to post the PR link, tests, final status, and residual risks.
tools:
  linear:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: linear
      connection: linear
      optional: true
```

The handoff arrives through `auto.sessions.spawn`, so Shepherd needs the Linear chat tool but no duplicate Linear event trigger. Merge to your production branch for [GitHub Sync](/concepts/github-sync) to apply.

## The full configuration

```text theme={null}
.auto/
  agents/issue-triage.yaml
  agents/shepherd.yaml
  assets/triage.png
  assets/patch.png
  fragments/environments/agent-runtime.yaml
```

```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/issue-triage.yaml theme={null}
name: issue-triage
model:
  provider: anthropic
  id: claude-opus-4-8
identity:
  displayName: Issue Triage
  username: issue-triage
  avatar:
    asset: .auto/assets/triage.png
  description: Triages labeled issues - sets metadata, posts handoff context, queues Shepherd-ready work, and optionally notes it in Slack.
imports:
  - ../fragments/environments/agent-runtime.yaml
systemPrompt: |
  You are the issue triage agent for acme/widgets. Work from GitHub Issues
  as the source of truth, using issue_read, issue_write, and add_issue_comment
  through the github tool to inspect, update, and comment on issues.

  The `auto-triage` label is a one-shot request token, not a standing
  subscription. Remove it with issue_write once you have acted on the request.

  Triage responsibilities:
  - Identify duplicates; close or link them only when the match is clear,
    preserving important detail on the parent issue.
  - Rank priority from impact, urgency, user signal, and blocked work.
    Explain non-obvious priority changes in a GitHub issue comment.
  - Categorize with the most specific existing labels you can justify. Never
    create GitHub labels — if the expected label does not exist, note that in
    a comment and continue without it.
  - Split broad reports into targeted child issues when one issue mixes
    unrelated tracks; keep the parent as context.
  - Ask for missing reproduction steps, desired behavior, or acceptance
    criteria in a GitHub issue comment. Do not invent requirements.

  When an issue is clear enough to implement:
  - Comment on the issue with concise handoff context for Shepherd.
  - Remove the `auto-triage` label with issue_write.
  - Call auto.sessions.spawn with agent `shepherd` and a message carrying
    the issue number, title, URL, triage summary, acceptance criteria,
    and constraints. Tell Shepherd to open a PR against main with a Review
    Map section and to comment back on the GitHub issue with the PR link,
    tests run, and residual risks.
  - When the chat tool is available, also post a brief note in Slack #dev: a
    top-level message with only the issue link and a one-sentence reason it
    is ready, details threaded. Slack renders mrkdwn links:
    <https://url|text>.

  Slack reporting is optional zero-configuration wiring using the standard
  `slack` connection name and `#dev` channel. When the chat tool is
  unavailable, skip the Slack steps and do not treat that as a failure —
  GitHub comments remain the complete triage record.

  Keep changes small and reversible. Prefer comments that explain what you
  did over silent metadata churn. When posting GitHub comments, append this
  hidden attribution marker with the environment variables expanded:

    <!-- auto:v=1 session_id=$AUTO_SESSION_ID agent=$AUTO_AGENT_NAME -->
initialPrompt: |
  Triage GitHub issue #{{github.issue.number}}: {{github.issue.title}}

  Trigger event: {{type}}
  Issue URL: {{github.issue.htmlUrl}}

  Inspect the issue and related GitHub context with issue_read, then apply
  your triage instructions. Remember the `auto-triage` label is a one-shot
  request token — remove it with issue_write once you have acted.
mounts:
  - kind: git
    repository: acme/widgets
    mountPath: /workspace/repo
    ref: main
    auth:
      kind: githubApp
      capabilities:
        contents: read
        pullRequests: none
        issues: write
        checks: none
        actions: none
workingDirectory: /workspace/repo
tools:
  auto:
    kind: local
    implementation: auto
  github:
    kind: github
    tools:
      - issue_read
      - issue_write
      - add_issue_comment
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connections
      optional: true
      connections:
        - provider: slack
          connection: slack
triggers:
  - name: issue-opened
    event: github.issue.opened
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.auto.authored: false
    message: |
      A new issue was opened on acme/widgets:
      #{{github.issue.number}} — {{github.issue.title}}.

      Issue URL: {{github.issue.htmlUrl}}

      Inspect it with issue_read and apply your triage instructions. If it is
      implementation-ready, comment with handoff context, remove the
      `auto-triage` label if present, and spawn Shepherd.
    routing:
      kind: bind
      target: github.issue
      onUnmatched: spawn
  - name: issue-labeled
    event: github.issue.labeled
    connection: github-acme
    where:
      $.github.repository.fullName: acme/widgets
      $.github.auto.authored: false
      $.github.label.name: auto-triage
    message: |
      The `auto-triage` label was just added to acme/widgets
      issue #{{github.issue.number}} — {{github.issue.title}}.

      Issue URL: {{github.issue.htmlUrl}}

      The label is a one-shot re-triage request. Inspect the issue with
      issue_read, apply your triage instructions, then remove the label with
      issue_write once you have acted.
    routing:
      kind: bind
      target: github.issue
      onUnmatched: spawn
```

```yaml .auto/agents/shepherd.yaml theme={null}
name: shepherd
model:
  provider: anthropic
  id: claude-opus-4-8
identity:
  displayName: Shepherd
  username: shepherd
  avatar:
    asset: .auto/assets/patch.png
  description: Owns triaged issues through implementation, review, and final status reporting.
imports:
  - ../fragments/environments/agent-runtime.yaml
systemPrompt: |
  You are the Shepherd implementation owner for acme/widgets.

  Treat each run as fresh, scoped implementation work. Read the repo's
  contribution docs before editing. Keep the change scoped to the requested
  task; no broad refactors unless required for the fix.

  Work from the mounted checkout on main. Create a feature branch named
  from the issue number plus a short slug, for example
  `auto/issue-123-fix-pagination`.

  Prefer test-first for clear behavior changes: add a focused failing test,
  implement the smallest fix, make it pass. Run the relevant test and
  typecheck commands before opening a PR; document anything you had to skip
  and why.

  Commit with a concise message referencing the issue number, push the
  branch, and open a pull request against main with the create_pull_request
  tool. The PR body must include a Review Map section pointing reviewers at
  the riskiest files first.

  When posting GitHub comments or PRs, append this hidden attribution
  marker with the environment variables expanded:

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

  Comment back on the GitHub issue (add_issue_comment) with the PR link, the
  tests you ran, and residual risks.

  If requirements are blocked or tests cannot run, stop and explain the
  blocker instead of inventing a solution.
initialPrompt: |
  Implement the issue described in the spawn message. Follow your profile
  instructions: scoped change, focused tests, a PR against main with a
  Review Map, and a closing comment on the GitHub issue.
mounts:
  - kind: git
    repository: acme/widgets
    mountPath: /workspace/repo
    ref: main
    auth:
      kind: githubApp
      capabilities:
        contents: write
        pullRequests: write
        issues: write
        checks: read
        actions: read
workingDirectory: /workspace/repo
tools:
  auto:
    kind: local
    implementation: auto
  github:
    kind: github
    tools:
      - pull_request_read
      - create_pull_request
      - issue_read
      - add_issue_comment
```

The permission split is the safety story: triage mounts the repo read-only with `issues: write` and nothing else (`pullRequests: none`), while Shepherd gets `contents: write` and `pullRequests: write` but is only ever started with a brief that triage already validated. Filtering both GitHub triggers on `$.github.auto.authored: false` keeps issues opened by auto's own GitHub App from re-triggering triage in a loop.

<Note>
  Plain-issue comment events use the `github.issue.comment.*` keys;
  `github.issue_comment.*` is GitHub's PR-comment family. And on GitHub events
  the authored flag lives at `$.github.auto.authored` — `$.auto.authored` is the
  chat-event path. See [GitHub events](/reference/events/github).
</Note>

## Walkthrough

<Steps>
  <Step title="An issue opens">
    `github.issue.opened` arrives; `issue-opened` matches. No session is bound
    to this issue, so `onUnmatched: spawn` starts a triage session bound to the
    `github.issue` target.
  </Step>

  <Step title="Triage acts on the issue">
    The session inspects the issue with `issue_read`, checks for duplicates,
    sets the most specific existing labels and a priority with `issue_write` (it
    never creates labels), and posts one comment explaining what it did — or
    asking for reproduction steps and acceptance criteria when the report is too
    thin.
  </Step>

  <Step title="A human requests a re-pass">
    Later, someone adds the `auto-triage` label. `github.issue.labeled` with
    `$.github.label.name: auto-triage` matches; the `bind` route delivers into
    the still-live triage session, or spawns a fresh one bound to the issue if
    the original is gone. Triage acts, then removes the label — consuming the
    token.
  </Step>

  <Step title="Handoff to Shepherd">
    When the issue is implementation-ready, triage comments handoff context on
    the issue, removes the label, and calls `auto.sessions.spawn` with `agent:
            shepherd` and the full brief. The tool returns the new session (with its
    URL), so the handoff is traceable from either session.
  </Step>

  <Step title="Shepherd ships a PR">
    The Shepherd session boots with the repo mounted at `main`, creates
    `auto/issue-123-…`, works test-first, pushes, opens a PR with a Review Map
    via `create_pull_request`, and closes the loop with `add_issue_comment`: PR
    link, tests run, residual risks. If a [code review
    agent](/examples/code-review) is installed, it picks the PR up from
    `github.pull_request.opened`.
  </Step>
</Steps>

## Linear variant

Teams that track work in Linear import the `-linear` entrypoints. The shape changes in three ways: the trigger source becomes `linear.issue.*` events, the label is the *only* request token (there is no every-new-issue trigger), and issue metadata is read and mutated through the unified [chat tool](/runtime/chat-tools)'s `chat.issue.get` / `chat.issue.update` instead of the GitHub tool.

```yaml .auto/agents/issue-triage.yaml theme={null}
name: issue-triage
imports:
  - "@auto/agents@latest/issue-triage-linear.yaml"
variables:
  repoFullName: acme/widgets
  linearConnection: linear
```

The Linear triggers fire on creation with the label present, and on update only when the label was just added — `linear.updatedFrom.labelNames.added` carries exactly the labels the update added:

```yaml theme={null}
triggers:
  - name: issue-created
    event: linear.issue.created
    connection: linear
    where:
      $.linear.issue.labelNames:
        contains: auto-triage
    routing:
      kind: spawn
  - name: issue-labeled
    event: linear.issue.updated
    connection: linear
    where:
      $.linear.updatedFrom.labelNames.added:
        contains: auto-triage
    routing:
      kind: spawn
```

Payload placeholders follow the Linear namespace: `{{linear.issue.identifier}}`, `{{linear.issue.title}}`, `{{linear.issue.url}}`. Shepherd still needs the GitHub App mount to open PRs; it reports back on the Linear issue via `chat.send` with target provider `linear`. See [Linear events](/reference/events/linear).

## Variations

* **Slack visibility.** The managed template's base entrypoints already carry `optional: true` Slack wiring — an ad-hoc `@mention` trigger on each agent plus triage's ready-work notes in `#dev` — which apply skips while no `slack` connection exists and activates on the next apply once one does. The handwritten configuration above keeps triage's chat tool but trims the mention triggers; copy the `mention` trigger from the [code review example](/examples/code-review) to restore them.
* **Direct coding handoffs.** For "just fix it" requests on issues (a human mentions the handoff agent in an issue comment), the [handoff example](/examples/handoff) is purpose-built: it keeps the issue binding, opens a PR, binds it as `github.pull_request`, and reports back on the issue.
* **Raise or lower the handoff bar.** The definition of "implementation-ready" lives entirely in the triage `systemPrompt`. Tighten it (require acceptance criteria and a reproduction) for riskier codebases; loosen it for internal tools.
* **Guard the spend.** Add [`spendCaps`](/reference/runtime-controls) to `shepherd` (for example `spendCaps: { maxPerSessionUsd: "10" }`) so a runaway implementation session self-limits.
* **Smoke test.** After apply, open a test issue and confirm a triage session spawns and comments. Then label an implementation-ready issue `auto-triage` and watch Shepherd open a PR. You can also start either agent directly from the web app with a message like "Triage issue #42".
