Repository: cli/cli

Issue:
`gh auth status` should have a JSON output option
### Describe the feature or problem you’d like to solve

There isn't a good way to easily integrate tools with the `gh` command to use the token. For example to create a Docker credential helper like in #5150 because the current output of `gh auth status` is formatted for human viewing and isn't guaranteed to remain stable.

### Proposed solution

The command `gh auth status` should have an optional JSON output option to allow for integration with other tools that might need to use a Github token.

You can get just the token with `gh auth token` but it could be helpful to have other information such as the username, and the scopes. That way if necessary a command could run a `gh auth refresh --scopes <scope>` if the appropriate scope is not available. So for example a Docker credential helper as mentioned above could check if the token has the "read:packages" or even "write:packages" scope if necessary to push.

### Additional context

An example output could be something like
```json
{
    "github.com": {
        "active": true,
        "token": "gho_*******",
        "scopes": [
            "gist",
            "repo",
            "write:packages"
        ]
    }
}
```


Hints from issue comments:
@williammartin: Hey @evilhamsterman thanks for opening this enhancement request. It generally seems sensible to me though I'm curious if you have a _specific_ need as opposed to a theorised one around docker credential managers? I'm not at all opposed to the idea but I would like the design to be motivated by some real need, if that makes sense. Cheers!@evilhamsterman: @williammartin it isn't a theorized one, #5150 has been open for a while without any apparent movement. I was looking to go ahead and build one as my company is looking to use ghcr for internal containers and other packages.

Another example in my company, I have written scripts to help administer various aspects of our GitHub organizations. I'd prefer to grab the token from gh than have them generate and manage PATs, or setup my own GitHub app. We already use gh and takes care of storing things rather than having to reinvent the wheel for small scripts.

I can get the token from gh auth token, but I also would like to be able to check that the token has the appropriate scopes for the appropriate orgs. I can do that by either trying to parse the existing output, or by making a general API call and inspecting the headers returned, but it would be much easier and quicker to just call the command, get a json object to parse and grab the values needed.@williammartin: @evilhamsterman, sure I'm familiar with that issue. My point was less "is a docker credential helper needed" and more "is this going to help someone build a docker credential helper right now" because having a real user of this change is important both for design and prioritisation. It sounds like if we added support for this it would help you right now.

It generally sounds like a reasonable idea to me, I'm just gonna tag @andyfeller on here in case he has any additional thoughts.@evilhamsterman: Thanks it'll also make my life easier for work@vedosis: Here's a real world example:

https://xbarapp.com/docs/plugins/Dev/GitHub/pull-requests.5m.js.html

Being able to use automation to:
1. fetch your current tokens (rather than hard-coding the values)
2. ping a GitHub API with a specific query + tokens
3. send off a notification or flash your IoT lights 🤷🏻 @andyfeller: @evilhamsterman, @williammartin: Thank you for your patience catching up! 🙇  My main thought stems from a security perspective, which I think is minimized as I elaborate below.

### Security concerns 🤔 

My main concern, which I believe is abated, relates to ["Security hardening for GitHub Actions"](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-secrets) given ["GitHub CLI use also in GitHub Actions workflows"](https://docs.github.com/en/actions/using-workflows/using-github-cli-in-workflows):

> **Never use structured data as a secret**
> 
> Structured data can cause secret redaction within logs to fail, because redaction largely relies on finding an exact match for the specific secret value. For example, do not use a blob of JSON, XML, or YAML (or similar) to encapsulate a secret value, as this significantly reduces the probability the secrets will be properly redacted. Instead, create individual secrets for each sensitive value.

In the case of GitHub Actions, this required the person who created a workflow or custom action to intentionally know how the secrets used worked and handling explicitly masking them.  Knowing it might be complicated if the GitHub CLI had to emit the necessary [`::add-mask` workflow commands](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#masking-a-value-in-a-log), I got confirmation from our `actions` friends that all of the [known GitHub token formats](https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/) are automatically masked as secrets without further intervention.

My secondary thought is whether we should intentionally disallow the `--jq` support for this command. 🤔  

After saying that, I realize practically there is nothing to stop a user from manipulating the output of `gh auth status --json` externally with `jq` or other tools, so it would merely be a hindrance with no real security.@williammartin: In an internal conversation I mentioned that I believe `--show-token` should still be required to remove the token mask even if `--json` is provided.@evilhamsterman: @andyfeller I appreciate the security concern, but I also feel that it isn't always you job to prevent a useful function that could improve security in several ways because of a concern for what could be an occasional issue in other situations. This feels like a "but sometimes" of software, https://youtu.be/GiYO1TObNz8.

In this case I think the the ability to more easily use `gh` to manage tokens in personal scripts vs something like generating and storing a PAT far out weighs the concern of use in something like GitHub Actions where most people will be using `GITHUB_TOKEN` directly rather than passing it to `gh` just to get it again from `gh auth status --json`. Plus as you said it should be masked automatically anyway. If it is really a major concern in somewhat I can't see you could disable the flag when running in an Action workflow. You can still get the token with `gh auth token` or `gh auth status --show-token` this is just to help scripts by also providing a format that's easy to parse not just the token but the features like the token scopes by machine. 

I'm not sure how limiting the `--jq` functionality would create any more security concerns than is already present in just showing the token. It's just a way to easily pick the data you want from the output. Especially since like you said there isn't anything stopping you from piping it through `jq` or `yq` yourself. In fact it could improve security because it would be easier to have `gh` output just the scopes if that's what you want.

@williammartin I can see the value in including `--show-token`  without `--json`,  but I personally think that if you are requesting a json output with `--json`, that already implies you have an idea of what you're doing and you want all the values so `--show-token` shouldn't be necessary. However that's a minor quibble and would not be a blocker.

Ultimately all the values that you would get from a `--json` flag are available already through other methods, really this is just a nice to have that enables easier integration of `gh` with other scripts and tools to securely manage your GitHub token. @williammartin: > I can see the value in including --show-token without --json, but I personally think that if you are requesting a json output with --json, that already implies you have an idea of what you're doing and you want all the values so --show-token shouldn't be necessary. However that's a minor quibble and would not be a blocker.

I don't want people to accidentally expose their token while exploring CLI behaviour.

--

In any case it sounds like we're all on board there is value in this, and we have a general picture of how it will work. I think the only outstanding piece is to design for the failure cases to be useful.@jessehouwing: I too have this need. I have a number of scripts I use to admin our enterprise and try not to be logged in with a `admin:enterprise` level token all the time for security reasons. I also don't want to go through web-auth and SSO approval of the token on every script run.

So having the ability to check the available token scoped (or a separate command that would allow me to test which scopes I have) would be quite handy.

What I'm missing from the output is which organizations the token has been authorized against, cause that would also be a reason to require a re-auth.

As for concerns around the token itself, for me it could be omitted from the result altogether.

There's already a `gh auth token` command that will happily dump the token to the console without qualm.

Alternatively `gh` could detect it's running in Actions and issue the `add-mask` to the error stream while dumping the json to stdout. I think that should work ;).@williammartin: I'm labelling this `help wanted` because I believe it's a good thing to have, it has a few reactions and https://github.com/cli/cli/issues/9326 was just opened as well.

I'd ask that anyone who picks this up also works with us to design carefully about how we represent the different states, actions that are suggested and exit codes. Some ideas are in https://github.com/cli/cli/issues/9326. We can also start with a subset that would go a long way to satisfying the majority of use cases.@benjlevesque: @williammartin I'd be happy to contribute to this.
Regarding your comment 
> design carefully about how we represent the different states, actions that are suggested and exit codes

Here are my design ideas: 
### State
- states: add a `status`  field, which can be one of "success", "error". 
- In case of `error`, add an `error_reason` field, which can be `timeout` or `invalid_token` and an `error_message` which could be the same message as the normal output (ie. "Timeout trying to log in [...]")
-  `You are not logged into [...]` should result in an empty object `{}`?

### Exit code
Expected behaviors like Success, No account, or Invalid Token should result in a 0 code. 
Timeout or other unexpected errors should result in a non-0 code (always 1?)

Optionally, we could add an extra flag (something like  `--expect-success`) to change this behavior and only exit 0 in case of success.

### Suggested actions
I feel that the JSON format is intended for automation, and does not require providing "help" as the regular output does. Hence I would omit the suggested actions (`gh auth login -h INSTANCE_NAME`)


### Token protection
The token should remain hidden unless specified otherwise (`--show-token`)

### JSON Structure
#### Option 1: flat array
```json
[
  {
    "status": "success",
    "active": true,
    "host": "github.com",
    "user": "USERNAME",
    "token": "gho_************************************",
    "token_source": "keyring",
    "git_protocol": "ssh",
    "scopes": "admin:org, gist, repo"
  },
  {
    "status": "error",
    "host": "ghe.io",
    "user": "USERNAME",
    "token_source": "GH_CONFIG_DIR/hosts.yml",
    "git_protocol": "https",
    "error_reason": "timeout",
    "error_message": "Timeout trying to log in to ghe.io account USERNAME (GH_CONFIG_DIR/hosts.yml)"
  }
]
```
#### Option 2: map with host
```json
{
  "github.com": [{
     // ...
  }],
  "ghe.io": [{
     // ...
  }]
}
```

Option 1 is my preferred one because the hostname at the top feels redundant; it seems easier to manipulate an array, especially since the command offers a `--hostname` flag which makes filtering easy. An extra `--username` flag could be added to simplify further. @briceburg: I like the implementation detailed in https://github.com/cli/cli/issues/8637#issuecomment-2246318117  -- having an -exit-code would help greatly to quickly determine if the `gh` cli has active credentials, configured correctly. 

My immediate use case is as part of an [interactive] developer bootstrap utility. The bootstrap needs to know if `gh` has been properly configured/authenticated so that it can make subsequent API calls... and if it has not, then the script could call `gh auth login` + `gh auth git-config` as an example. @evilhamsterman: Been a while since any activity on this it seems. I may take a stab at it. Personally I prefer the mapping format, as I suggested in my initial post. I'm not sure how it's easier to manipulate an array vs a map especially since the point of this is data retrieval not manipulation. You can filter with `--hostname` but perhaps you do want all and want to parse them somehow, using an array would mean you have to loop through all of them to collect everything, having a map would let you easily parse just the respective hosts. Having the host as the key doesn't have to be redundant, just omit it from the status object, or keep both and people can ignore it.

Expanding on my previous suggestion with @benjlevesque's additions

```json
{
    "github.com": [
        {
            "status": "success",
            "active": true,
            "host": "github.com",
            "user": "USERNAME",
            "token": "gho_************************************",
            "token_source": "keyring",
            "git_protocol": "ssh",
            "scopes": "admin:org, gist, repo",
        },
        {
            "status": "error",
            "host": "github.com",
            "user": "USERNAME",
            "token_source": "GH_CONFIG_DIR/hosts.yml",
            "git_protocol": "https",
            "error_reason": "invalid",
            "error_message": "Invalid token you must reauthenticate"
        }
    ],
    "ghe.io": [
        {
            "status": "success",
            "active": false,
            "host": "ghe.io",
            "user": "USERNAME",
            "token_source": "GH_CONFIG_DIR/hosts.yml",
            "git_protocol": "https",
            "error_reason": "timeout",
            "error_message": "Timeout trying to log in to ghe.io account USERNAME (GH_CONFIG_DIR/hosts.yml)"
        }
    ]
}
```

As to exit codes, I'd go ahead an always exit 0 unless the we can't return anything at all like if there are no accounts, or the `--hostname` doesn't exist. To me it doesn't make sense to exit with an error because one account out of several has an issue when the other's are fine. A lot of languages have an option to check if a called process exited with an error, example Python's `subprocess.popopen(check=True)`.

Too me it seems simpler to only throw an error if the request completely fails, then I can handle that path. If it at least partially succeeds then I can just check `hostname[].account.status==success`@BagToad: @babakks will write AC for this.@babakks: Sorry for the long comment here, but I think it's needed to shed light on small details here.

I'm now going through a quick break down of various scenarios and how `auth status` currently works. Then I'll dig into the machine-friendly output requirements and grab some ideas from #9326, and at the end, I'll share a *candidate A/C* so that we can talk about and polish it.

Please feel free to share your thoughts. 🍻

Also cc-ing @andyfeller and @BagToad.

## How `auth status` currently works

To recap, the `auth status` command iterates over all available tokens, including the `GH_TOKEN` env var, checks if they're still valid (by calling an API endpoint to retrieve their scopes), and build an itemised summary, each entry of which can be one of the following:
- a valid entry
- an invalid entry
- a timeout entry

A timeout entry, can be actually a valid/invalid one, but it's just `gh` couldn't confirm their status at the time of running.

Anyway, if there's at least one non-valid entry among the report, the command terminates with exit code 1.

But this is the happy path of `auth login`. For example, if there are no tokens available, or if then given `--hostname` option value was not among the known hosts, the command fails with exit code 1.

In other words, `auth status` exists with zero, only if all tokens are valid. **We strongly need to keep this behaviour intact to avoid breaking any usages.**

## Machine-friendly output

We're going to shape:
- a CLI experience (i.e. options and usage)
- a sensible exit code regime
- a JSON output structure

### 1. CLI experience

As I'm aware of, `gh` has two styles for enabling a JSON output:
1. **Combination of `--json <fields>`, `--jq <query>`, and `--template <template>` options,** like in `pr view` or `issue list`.
   Note that with the `--json` field it's required to provide the list of fields. I mean, you cannot just pass `--json` and expect the commands to retrieve all fields. The reason behind this design is that some fields require further API calls, which are sometimes expensive in terms of API burden and/or rate-limit capacity.
2. **Combination of `--format json`, `--jq <query>`, and `--template <template>` options,** like in `release verify`.

Based on this, I think the idiomatic way is to go with the second style, as there's no point/benefit in picking individual fields since they're readily available without further API calls. This is how a common use case of `auth status` would look like:

```sh
gh auth status --format json --jq '."github.com" | .[] | select (.state == "success") | .user'
```

### 2. Exit code

Sorry, but I disagree with @evilhamsterman in that we should only return with zero exit code. My personal reason is, as a user, I don't expect a command's exit code to be different based on a mere **formatting** option. Not saying there isn't such cases, just it's not a nice experience. And expectedly it'd be niche enough that users might miss it even if we clearly document it. I know, if I could go back in time, I would argue against the current behaviour, but I think we're tied to our path now.

Okay, if we're going to keep the exit codes as they're in the interactive flow, then we should note that whoever is calling `auth status` with the `--json` option, has all the rights to expect a valid JSON output regardless of the exit code. I personally hate to do a *try-parse* kind of thing.

So, this seems to be a legit regime in the machine-readable mode:
| When                                      | Exit code | stdout  | stderr                    |
| ----------------------------------------- | --------- | ------- | ------------------------- |
| no tokens available                       | 1         | `{}`    | *[current error message]* |
| no token available for given `--hostname` | 1         | `{}`    | *[current error message]* |
| one or more invalid/timeout entries       | 1         | `{...}` | [empty]                   |
| no invalid/timeout entries                | 0         | `{...}` | [empty]                   |

### 3. JSON structure

I agree with @williammartin that the `--show-token` option should still be respected. However, if the `--show-token` is not provided, it's obvious that showing a masked token in machine-readable output doesn't make any sense. So, the `token` field must only be present if the the `--show-token` is set.

I also think the map structure suggested by @evilhamsterman is more intuitive and straightforward to lookup for entries.

So, the JSON output can look like this (note the different types of entries):

```jsonc
{
    "hostname": [
        {
            // valid entry
            "state": "success",

            "active": true,               // or false
            "host": "hostname",           // redundant, but still spares holding parent key
            "user": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",               // present only if `--show-token` is provided
            "scopes": "...",
            "gitProtocol": "ssh",         // or "https"
        },
        {
            // invalid entry
            "state": "error",
            "error": "[error message]",

            "active": false,              // or true
            "host": "hostname",
            "user": "username",
            "tokenSource": "oauth_token", // or an env var name
        },
        {
            // timeout entry
            "state": "timeout",
            "error": "[error message]",

            "active": false,              // or true
            "host": "hostname",
            "user": "username",
            "tokenSource": "oauth_token", // or an env var name
        },
    ],
    // ...
}
```

## Candidate A/C

### No tokens
**Given** I have no auth tokens on my host machine
**When** I run `gh auth status --format json`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with exit code 1.

```
You are not logged into any GitHub hosts. To log in, run: gh auth login
```

### No token for the given `--hostname`
**Given** I have no auth tokens for a GitHub host named `foo.com` on my host machine
**When** I run `gh auth status --hostname foo.com --format json`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with exit code 1.

```
You are not logged into any accounts on foo.com
```

### All valid tokens
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --format json`
**Then** I get a JSON output like below (without token or its mask) with a **zero** exit code.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
    // other hosts...
}
```

### All valid tokens, with `--hostname`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --hostname github.com --format json`
**Then** I get a JSON output like below (without token or its mask) with a **zero** exit code. Note that there's only one hostname in the output.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
}
```

### All valid tokens, with `--show-token`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --show-token --format json`
**Then** I get a JSON output like below including the token as plaintext, with a **zero** exit code.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "token": "#################################",
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
    // other hosts...
}
```

### All valid tokens, with `--hostname` and `--show-token`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --hostname github.com --show-token --format json`
**Then** I get a JSON output like below including the token as plaintext, with a **zero** exit code. Note that there's only one hostname in the output.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "token": "#################################",
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
}
```

### An invalid token
**Given** I have an invalid auth token (among other tokens)
**When** I run `gh auth status --format json`
**Then** I get a JSON output like below with **exit code 1**.

```jsonc
{
    "github.com": [
        {
            "state": "error",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
        },
        // other accounts...
    ],
    // other hosts...
}
```

### An unreachable host (timeout)
**Given** I have a valid (or invalid, doesn't matter) auth token that for an unreachable GitHub host
**When** I run `gh auth status --format json`
**Then** I get a JSON output like below with **exit code 1**.

```jsonc
{
    "github.com": [
        {
            "state": "timeout",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
        },
        // other accounts...
    ],
    // other hosts...
}
```

### With `--jq`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --format json --jq <QUERY>`
**Then** I get the result of applying the given `<QUERY>` on the JSON data.

### With `--template`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --format json --template <TEMPLATE>`
**Then** I get the result of rendering the given `<TEMPLATE>` from the JSON data.
@evilhamsterman: @babakks that looks good. While I disagree on the exit code, that's my preference, it's also not a big problem to just ignore it when parsing and handle errors further up.

I haven't had much time to dig in , but earlier when poking around, I was thinking that a good course of action may be to have the command always essentially generate the json structure internally. Then the output is just formatting.

If you set `--format json` just dump the structure, with or without filtering for the token. If you don't then format it for the CLI output. 

That also opens up another option for people who want to use `gh` for authentication. If the auth values are split from the display logic and handled in its own module, then people writing Go could import the module directly in their own code.

It's a bit of a feature creep though. Just getting the json would be a big improvement @babakks: Thanks for your thoughts, @evilhamsterman!

Actually, the implementation is quite similar to what you just described. The only thing is to make the type implement the `Exportable` interface.

As for usage as a library, this module (i.e. `github.com/cli/cli/v2`) is not meant to be used as a dependency. That's why shared/common components are shipped via `github.com/cli/go-gh/v2`. But anyway, I get your point as an engineering practice to  reduce coupling among components and make them reusable.@BagToad: > Sorry, but I disagree with [@evilhamsterman](https://github.com/evilhamsterman) in that we should only return with zero exit code. My personal reason is, as a user, I don't expect a command's exit code to be different based on a mere **formatting** option. Not saying there isn't such cases, just it's not a nice experience. And expectedly it'd be niche enough that users might miss it even if we clearly document it. I know, if I could go back in time, I would argue against the current behaviour, but I think we're tied to our path now.

Do you know what the precedence is with our other command sets? I'm not passing an opinion here, but I'd like to understand where this proposed behavior lies with our other command sets' behavior.

Let's take `gh pr checks` as a single example. We exit 1 when we have at least one failed PR check.

<details><summary>example gh pr checks failure</summary>
<p>

```
❯ gh pr checks            
Some checks were not successful
0 cancelled, 1 failing, 10 successful, 0 skipped, and 0 pending checks

   NAME                                                               DESCRIPTION  ELAPSED  URL                                                             
X  Lint/govulncheck (pull_request)                                                 52s      https://github.com/cli/cli/actions/runs/16782852957/job/47525...
✓  CodeQL                                                                          2s       https://github.com/cli/cli/runs/47525970617
✓  Code Scanning/CodeQL-Build (actions) (pull_request)                             1m7s     https://github.com/cli/cli/actions/runs/16782852962/job/47525...
✓  Code Scanning/CodeQL-Build (go) (pull_request)                                  4m12s    https://github.com/cli/cli/actions/runs/16782852962/job/47525...
✓  Unit and Integration Tests/build (macos-latest) (pull_request)                  7m17s    https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/build (ubuntu-latest) (pull_request)                 5m22s    https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/build (windows-latest) (pull_request)                11m45s   https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/integration-tests (macos-latest) (p...               1m8s     https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/integration-tests (ubuntu-latest) (...               56s      https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/integration-tests (windows-latest) ...               5m13s    https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Lint/lint (pull_request)                                                        2m12s    https://github.com/cli/cli/actions/runs/16782852957/job/47525...

❯ echo $?
1

```

</p>
</details>

But, let's check the `--json`:

<details><summary>example gh pr checks with --json</summary>
<p>


```
❯ gh pr checks --json name
[
  {
    "name": "CodeQL"
  },
  {
    "name": "build (macos-latest)"
  },
  {
    "name": "integration-tests (macos-latest)"
  },
  {
    "name": "build (ubuntu-latest)"
  },
  {
    "name": "integration-tests (ubuntu-latest)"
  },
  {
    "name": "integration-tests (windows-latest)"
  },
  {
    "name": "CodeQL-Build (go)"
  },
  {
    "name": "lint"
  },
  {
    "name": "CodeQL-Build (actions)"
  },
  {
    "name": "build (windows-latest)"
  },
  {
    "name": "govulncheck"
  }
]

❯ echo $?                 
0

```

</p>
</details> 


As you can see, it exits 0.

If we choose to make `gh auth status`'s JSON output fail too, we are deviating from `gh pr checks`. I think `gh` users value behavior consistency between command sets, so I ask that we please make sure that this is definitely worth breaking convention before proceeding with it. 

And I also ask that other people give their knowledge of other command sets behavior because perhaps `gh pr checks` is an outlier and I'm just missing how other command sets do it. 


-----

> I agree with [@williammartin](https://github.com/williammartin) that the `--show-token` option should still be respected. However, if the `--show-token` is not provided, it's obvious that showing a masked token in machine-readable output doesn't make any sense. So, the `token` field must only be present if the the `--show-token` is set.

Can you talk more about this? I am actually under the impression that always using the same schema would be more valuable than conditionally removing `token` from the output schema.

In other words, I'd prefer if `token` was always in the output and that we obfuscate the value unless `--show-token` is called. 

Especially if we're not doing the typical `--json fields...` format and just doing `--format json`. I want the output schema to be fixed.

-----

> That also opens up another option for people who want to use `gh` for authentication. If the auth values are split from the display logic and handled in its own module, then people writing Go could import the module directly in their own code.

> As for usage as a library, this module (i.e. `github.com/cli/cli/v2`) is not meant to be used as a dependency. That's why shared/common components are shipped via `github.com/cli/go-gh/v2`. But anyway, I get your point as an engineering practice to reduce coupling among components and make them reusable.

@babakks @evilhamsterman To elaborate, in case it is useful, you can already use our [`auth` package in `cli/go-gh`](https://github.com/cli/go-gh/blob/a08820a13f257d6c5b4cb86d37db559ec6d14577/pkg/auth/auth.go#L31-L36) to help with producing authentication tokens. 
@andyfeller: My main feedback here is 1) **trying to stick with `--json <fields>` experience we use across virtually all commands** and 2) **use `login` instead of `user`**:

1. Consistent experience means no surprises for users
2. It reuses the existing `--json` logic to handle showing or not showing the token
3. It would reduce the number of flags for exporting the information from 2 to 1
4. I don't want to assume all sessions are tied to users but might be apps or bots, so the `login` verbiage would be general enough

Putting aside whether `host` is contained in a given host array entry, `--json active,gitProtocol,login,token,scopes,state` would allow users to choose what they export using the same experience as other commands without needing a separate flag for including tokens:

```json
{
  "github.com": [
    {
      "state": "success",
      "active": true,
      "host": "github.com",
      "user": "my-username",
      "tokenSource": "...", 
      "scopes": "...",
      "gitProtocol": "ssh",
    },
    // other accounts...
  ],
  // other hosts...
}
```

I don't think `--show-token` should really affect `--json` as someone would explicitly be asking for `token` field and should know it will be plaintext, $0.02

So essentially:

```shell
$ gh auth status --help
Display active account and authentication state on each known GitHub host.

For each host, the authentication state of each known account is tested and any issues are included in the output.
Each host section will indicate the active account, which will be used when targeting that host.
If an account on any host (or only the one given via `--hostname`) has authentication issues,
the command will exit with 1 and output to stderr.

To change the active account for a host, see `gh auth switch`.


USAGE
  gh auth status [flags]

FLAGS
  -a, --active            Display the active account only
  -h, --hostname string   Check only a specific hostname's auth status
      --json fields       Output JSON with the specified fields
  -t, --show-token        Display the auth token

INHERITED FLAGS
  --help   Show help for command

JSON FIELDS
  active, gitProtocol, login, token, scopes, state

LEARN MORE
  Use `gh <command> <subcommand> --help` for more information about a command.
  Read the manual at https://cli.github.com/manual
  Learn about exit codes using `gh help exit-codes`
  Learn about accessibility experiences using `gh help accessibility`
```
@babakks: Thanks for sharing your thought! @evilhamsterman @BagToad @andyfeller 

### Regarding the `--json` vs `--format` option

I'm okay to go with `--json <FIELDS>`, and also like the fact that this removes the need for the `--show-token` option. However, I think we should add a mutually exclusive restriction on using `--show-token` and `--json` option with an error message like:

```
`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.
```

### Regarding the exit code

I couldn't even imagine the mentioned behaviour in `pr checks`, so thank you @BagToad for the hint. As I checked this behaviour is not even documented, which I think we should do that. That said, I'm torn between doing the keeping things consistent with `pr checks`, and the user-friendly choice (keep the exit code regime as is).

Given the number of votes on this issue, I can imagine there are probably a good number of users who have managed to parse the current `auth status` output. Once they discover the new `--json` option, they'd probably go for it (I would 😄) and that's my fear for them to see an unexpected behaviour. I know they are expected to read the docs, but still.

An argument in favour of keeping the current exit codes is that if we decide to exit with zero regardless of the token status, then a user who just wants to see if all tokens are valid, has to go over all entries and assert they're all `success`. They would also need to assert that stderr output is empty. An alternative way for the user is to just use `auth status` without the `--json` fields first and if it was a success then they'd go and call it with the `--json` option to grab information they needed. I don't know if it's a real scenario out there or not, but if it is it's a mild headache for the user.

> [!NOTE]
> Looking around, I noticed a rather interesting approach in `run view` and `run watch`. There's this `--exit-status` option that when provided enforces the command to exit with a non-zero exit code if the any of the jobs (or the single one provided via `--job` option) has failed. So, the default behaviour is to always exit with zero, unless `--exit-code` is there. We're not employing such an option in other places (at least to my knowledge and searches). I'd be nice if we had it for the `auth status` from the beginning, and we wouldn't have to decide things now.

Anyway, I think that's all I have in favour of keeping the exit codes. Let me know what you think @BagToad, and we can finalise the A/C.

### Regarding the field names

I agree with `login` instead of `user`.@BagToad: ## Exit codes

@babakks and I discussed this and we agreed that always exiting 0 with `--json` makes sense. This aligns with other commands with custom exit code functionality like `gh pr checks` where JSON output causes a 0 exit code. 

Exiting non-zero in exceptional cases where valid JSON cannot be returned (e.g. a panic), is still appropriate, of course. 

We definitely need the documentation of this exit code behavior to be added to the AC. 

## JSON option formats

@babakks and I discussed the pros and cons of the two options

1. `--format JSON`, requiring `--show-token` to unmask the token.
2. `--json <fields>`, NOT requiring `--show-token` to unmask the token (token is always unmasked in JSON output)

We decided that option 2 ☝️ makes the most sense, all things considered because the user must explicitly ask for the token. This aligns with decisions that have been made with `gh auth token` as well where we print the token in plain text because we trust that's what the user asked for. Pairing that with `--json <fields>` just being largely the more consistent option with other commands, makes this the best option, we think.
@babakks: 
Thanks, @BagToad, for the summary! 🙏

Now that we're going with the `--json <FIELDS>` option, the JSON entries should all have the same set of fields, regardless of their token status. So, here's the expected JSON structure (if all available fields were requested):

```jsonc
{
    "hostname": [
        {
            // valid token entry
            "state": "success",
            "error": "",                  // empty
            "active": true,               // or false
            "host": "hostname",
            "login": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",
            "scopes": "...",
            "gitProtocol": "ssh",         // or "https"
        },
        {
            // invalid token entry
            "state": "error",
            "error": "[error message]",
            "active": false,              // or true
            "host": "hostname",
            "login": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",
            "scopes": "",                 // empty
            "gitProtocol": ""             // empty
        },
        {
            // timeout entry
            "state": "timeout",
            "error": "[error message]",
            "active": false,              // or true
            "host": "hostname",
            "login": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",
            "scopes": "",                 // empty
            "gitProtocol": ""             // empty
        },
    ],
    // ...
}
```

## Acceptance Criteria

> [!NOTE]
> - `<ALL-FIELDS>` is a placeholder for all fields.
> - `<ANY-FIELDS>` is a placeholder for any subset of the available fields.

### No tokens
**Given** I have no auth tokens on my host machine
**When** I run `gh auth status --json <ANY-FIELDS>`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with a zero exit code.

```
You are not logged into any GitHub hosts. To log in, run: gh auth login
```

### No token for the given `--hostname`
**Given** I have no auth tokens for a GitHub host named `foo.com` on my host machine
**When** I run `gh auth status --hostname foo.com --json <ANY-FIELDS>`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with a zero exit code.

```
You are not logged into any accounts on foo.com
```

### All valid tokens
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --json <ALL-FIELDS>`
**Then** I get a JSON output like below with a zero exit code.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "error": "",                                // empty
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...", 
            "token": "###############################", // (not a mask)
            "scopes": "...",
            "gitProtocol": "..."
        },
        // other accounts...
    ],
    // other hosts...
}
```

### All valid tokens, with `--hostname`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --hostname github.com --json state,active,host,login,tokenSource,scopes,gitProtocol,token`
**Then** I get a JSON output like below with a zero exit code. Note that there's only one hostname in the output.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "error": "",                                // empty
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...", 
            "token": "###############################", // (not a mask)
            "scopes": "...",
            "gitProtocol": "...",
        },
        // other accounts...
    ],
}
```

### An invalid token
**Given** I have an invalid auth token (among other tokens)
**When** I run `gh auth status --json <ALL-FIELDS>`
**Then** I get a JSON output like below with zero exit code.

```jsonc
{
    "github.com": [
        {
            "state": "error",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...", 
            "token": "###############################", // (not a mask)
            "scopes": "",                               // empty
            "gitProtocol": "",                          // empty
        },
        // other accounts...
    ],
    // other hosts...
}
```

### An unreachable host (timeout)
**Given** I have a valid (or invalid, doesn't matter) auth token that for an unreachable GitHub host
**When** I run `gh auth status --json <ALL-FIELDS>`
**Then** I get a JSON output like below with a zero exit code.

```jsonc
{
    "github.com": [
        {
            "state": "timeout",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...",
            "token": "###############################", // (not a mask)
            "scopes": "",                               // empty
            "gitProtocol": "",                          // empty
        },
        // other accounts...
    ],
    // other hosts...
}
```

### With `--jq`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --json <ANY-FIELDS> --jq <QUERY>`
**Then** I get the result of applying the given `<QUERY>` on the JSON data.

### With `--template`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --json <ANY-FIELDS> --template <TEMPLATE>`
**Then** I get the result of rendering the given `<TEMPLATE>` from the JSON data.

### `--show-token` and `--json` usage
**When** I run `gh auth status --show-token --json <ANY-FIELDS>`
**Then** I get the error below with exit code 1.

```
`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.
```@babakks: *This comment is for anyone who is interested to work on a PR for this issue.*

In the current implementation of the `auth status` command, there are different types for different token entries (e.g., `validEntry`, `timeoutErrorEntry`, or `invalidTokenEntry`). Although it seems like a fair design, now that we're going to introduce the JSON output it's much simpler to use a single type for entries. So, you might want to get rid of the existing types and consolidate them into a single type.

Also note that, fields like `tokenIsWriteable` can be dropped in favour using the `authTokenWriteable` function when it's needed.

With a single type for entries, you can simply use the below approach for making the type JSON exportable without any efforts (i.e. iterating over the fields, etc). 

https://github.com/cli/cli/blob/ca68f44ac7659a24f4bbc16ed9f160b56d4d6e3c/pkg/cmd/label/shared.go#L9-L33

You can also look at these examples on how to add the `--json`, `--jq`, and `--template` fields. Note that `cmdutil.AddJSONFlags` takes care of adding all three options.

https://github.com/cli/cli/blob/ca68f44ac7659a24f4bbc16ed9f160b56d4d6e3c/pkg/cmd/label/list.go#L79 

https://github.com/cli/cli/blob/ca68f44ac7659a24f4bbc16ed9f160b56d4d6e3c/pkg/cmd/label/list.go#L104-L107
@BagToad: @benjlevesque @evilhamsterman 👋 Since you folks expressed interest in taking a stab at this, it is ready for implementation following the acceptance criteria in https://github.com/cli/cli/issues/8637#issuecomment-3167167771. If anyone else is also interested, let us know, we can assign whoever to the issue 👍 @benjlevesque: Hey @BagToad thanks for the ping, I'd be happy to give this a try :) 

The AC seems pretty clear overall, but I have a few comments 

## Conflict in flag alias
 I noticed is that the `-t` short flag of `--show-token` conflicts with `--template` when using `AddJSONFlags`. What do you think should be done about this? Removing `-t` for `--show-token` would be a breaking change... Maybe make `-f` optional in  `AddJSONFlags`? I'll look into options...
 
## Error message for `--show-token` and `--json` usage
I've seen in other parts of the code that `MarkFlagsMutuallyExclusive` is used to mark flags exclusive. Would this be an acceptable solution, rather than a custom message?


@BagToad: > I noticed is that the `-t` short flag of `--show-token` conflicts with `--template` when using `AddJSONFlags`. What do you think should be done about this? Removing `-t` for `--show-token` would be a breaking change... Maybe make `-f` optional in `AddJSONFlags`? I'll look into options...

@benjlevesque that is a good question!

I think we should cut the short flag in this case 🤔 It sounds like you're on the right track with what you're looking into regarding `AddJSONFlags` 👍 Interested to see what it looks like in a PR and maybe we could spot other options.


> I've seen in other parts of the code that `MarkFlagsMutuallyExclusive` is used to mark flags exclusive. Would this be an acceptable solution, rather than a custom message?

I think that's reasonable. While I think it would be a slightly better user experience to have an explanation for why they're mutually exclusive like the AC suggests, if it makes the code unreasonable or requires way more work, then I don't think that specific error message should block the PR. Especially if we have the reason documented in the command's `Long` help doc output 🤔 

WDYT?@benjlevesque: @BagToad thanks for the response! I found a pretty good option for the `-t` conflict, let me know what you think in the [PR](https://github.com/cli/cli/pull/11544) :)

Regarding the exclusive flags, I do agree the message is clearer, I was only asking for consistency with other commands.

Please take a look at the PR and let me know what you think! 😊 

___

On another note, after testing the  `--json`, `--jq ` and `--template` options, I still find a flat array output (without `"github.com") would improve the UX.  Two main pain points:
 1. The --json fields don’t align with the top-level properties of the returned object.
 2. Using --template/--jq feels unnecessarily complex for this use case.

```bash
gh auth status --json login --template '{{ range (index . "github.com") }}{{ .login }}{{ end }}'
gh auth status --json login  --jq '.["github.com"].[].login'
```

With a flat array, it could be 
```bash
gh auth status --json login --hostname github.com --jq '.[].login' 
gh auth status --json login,host --jq '.[] | select(.host=="github.com") | .login'

gh auth status  --json login --hostname github.com  --template '{{ range . }}{{ .login }}{{ end }}' 
```

f you get a chance to test the flat array approach, I’d love to hear your thoughts about which approach feels more natural / easier to use 👍 @BagToad: Thank you @benjlevesque! 🙌 

I'm not sure about the flat array thing, but it sounds like it would be better 🤔 I would like @babakks's thoughts. 

----

> I found a pretty good option for the `-t` conflict, let me know what you think in the [PR](https://github.com/cli/cli/pull/11544) :)

Very cool idea! I am going to give it some thought to understand the impact of something like this, just in case 🤔 I'll comment more on the PR if I think of anything. @benjlevesque: @BagToad Regarding the `-t` flag, I didn’t verify if it would conflict with other flags, but I don’t think there’s a risk of collision since the code was crashing during execution when registering the second flag.

However, the current implementation has a downside: it could mask future conflicts by silently disabling the `-t` alias for `--template`. This might introduce breaking changes down the line, so it may not be the best approach.

Initially, I considered creating a separate function to register JSON flags without aliases and then manually adding the aliases afterward.

Let me know what you think!

@babakks: Thanks for picking up this work, @benjlevesque! 🍻 

Good call out regarding the `-t` conflict. I didn't notice it when I was writing the A/C. I Just commented (https://github.com/cli/cli/pull/11544#discussion_r2307131309) on your PR with a suggestion. Let's discuss there on the comment.

About the array/map structure, I have a few arguments in favour of maps:
- It's consistent with our human-friendly output which is grouped by hostnames.
- Looking up for a specific hostname is easier with a map.
- I kind of feel like most of the time automation scripts would prefer working with the JSON output and the `--jq` option over `--template`. It's not a 100% correct, of course, but to me the `--template` option is more like a way of reshaping the data for human readability.
- If a flat array is needed, (the seemingly redundant) `hostname` field is there in the entries which allows users to simply flatten the map without the need to somehow keep the map key. For example, jq's `add` function does what's needed:
  ```sh
  echo '{"foo":[{"bar":0},{"bar":1}],"baz":[{"bar":2}]}' | jq add
  # [
  #   {
  #     "bar": 0
  #   },
  #   {
  #     "bar": 1
  #   },
  #   {
  #     "bar": 2
  #   }
  # ]  
  ```

  Since some folks might prefer the flat array we can surely add an example in the `--help` docs to show them how to do it.

All said, I'm open to discussion if you strongly feel toward the flat array, @benjlevesque @BagToad @srid: FWIW: my main use case is to _programatically_ tell if the user has already `gh auth login`'ed once. @evilhamsterman: @srid it looks like @benjlevesque has a pr just about done so we should get this soon 🎆 @babakks: The PR for this is merged now and the JSON output will be available with the next release, which will be `v2.81.0`.

However, we ended up formatting the JSON output like this (note the top-level `hosts` field):

```jsonc
{
  "hosts": {
    "github.com": [/*...*/],
    "another.host": [/*...*/],
  }
}
```

This structure makes room for future extensibility without breaking automations (e.g. adding more top-level fields without them being interpreted as yet another host). This is more consistent with other `gh` commands. Also it'd be much easier for the user to invoke the command without the need to specify fields per entry, i.e.:

```
gh auth status --json hosts
```

To include the token in the JSON output, users can just add the `--show-token` (or `-t`):

```
gh auth status --json hosts --show-token
```@evilhamsterman: WOOHOO!  🥳 

```
$ gh auth status --json hosts
{
  "hosts": {
    "github.com": [
      {
        "state": "success",
        "active": true,
        "host": "github.com",
        "login": "evilhamsterman",
        "tokenSource": "keyring",
        "scopes": "gist, read:org, repo, workflow",
        "gitProtocol": "https"
      }
    ]
  }
}
```
Thanks so much @benjlevesque and @babakks! Now at some point when I get time I'll work on building a Docker credential plugin using `gh`

Solution patch:
diff --git a/.github/workflows/bump-go.yml b/.github/workflows/bump-go.yml
index 41a1a6a81..c5ba37ef0 100644
--- a/.github/workflows/bump-go.yml
+++ b/.github/workflows/bump-go.yml
@@ -13,7 +13,7 @@ jobs:
         uses: actions/checkout@v5
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
 
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 6d101184f..1e3cd897c 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -29,7 +29,7 @@ jobs:
 
       - name: Setup Go
         if: matrix.language == 'go'
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: "go.mod"
 
diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml
index 1510ac22a..09267a2d2 100644
--- a/.github/workflows/deployment.yml
+++ b/.github/workflows/deployment.yml
@@ -46,7 +46,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v5
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
       - name: Install GoReleaser
@@ -81,7 +81,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v5
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
       - name: Configure macOS signing
@@ -146,14 +146,14 @@ jobs:
  
   windows:
     needs: validate-tag-name
-    runs-on: windows-latest
+    runs-on: windows-2022
     environment: ${{ inputs.environment }}
     if: contains(inputs.platforms, 'windows')
     steps:
       - name: Checkout
         uses: actions/checkout@v5
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
       - name: Install GoReleaser
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 41b0ed00d..29ef76847 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -21,7 +21,7 @@ jobs:
         uses: actions/checkout@v5
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: "go.mod"
 
@@ -48,7 +48,7 @@ jobs:
         uses: actions/checkout@v5
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: "go.mod"
 
diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml
index f82df951a..7cc113878 100644
--- a/.github/workflows/govulncheck.yml
+++ b/.github/workflows/govulncheck.yml
@@ -13,7 +13,7 @@ jobs:
         uses: actions/checkout@v5
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
 
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 162c8499b..faf705b9b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -22,7 +22,7 @@ jobs:
         uses: actions/checkout@v5
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
 
@@ -54,7 +54,7 @@ jobs:
         uses: actions/checkout@v5
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
 
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
index 8939e4387..543a909c8 100644
--- a/.github/workflows/stale-issues.yml
+++ b/.github/workflows/stale-issues.yml
@@ -10,7 +10,7 @@ jobs:
   mark-stale-issues:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@v9
+      - uses: actions/stale@v10
         with:
           start-date: "2025-07-10T00:00:00Z"  # Skip for issues created before this date
           days-before-issue-stale: 30
diff --git a/.github/workflows/third-party-licenses.yml b/.github/workflows/third-party-licenses.yml
index b57639c89..0a9c8d32e 100644
--- a/.github/workflows/third-party-licenses.yml
+++ b/.github/workflows/third-party-licenses.yml
@@ -26,7 +26,7 @@ jobs:
           ref: trunk
 
       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version-file: 'go.mod'
 
diff --git a/api/queries_pr.go b/api/queries_pr.go
index 525418a11..b3373a903 100644
--- a/api/queries_pr.go
+++ b/api/queries_pr.go
@@ -62,6 +62,7 @@ type PullRequest struct {
 	MergedBy            *Author
 	HeadRepositoryOwner Owner
 	HeadRepository      *PRRepository
+	Repository          *PRRepository
 	IsCrossRepository   bool
 	IsDraft             bool
 	MaintainerCanModify bool
@@ -251,8 +252,9 @@ type Workflow struct {
 }
 
 type PRRepository struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
+	ID            string `json:"id"`
+	Name          string `json:"name"`
+	NameWithOwner string `json:"nameWithOwner"`
 }
 
 type AutoMergeRequest struct {
diff --git a/api/queries_repo.go b/api/queries_repo.go
index 324200afd..e797cead4 100644
--- a/api/queries_repo.go
+++ b/api/queries_repo.go
@@ -141,9 +141,10 @@ type RepositoryOwner struct {
 }
 
 type GitHubUser struct {
-	ID    string `json:"id"`
-	Login string `json:"login"`
-	Name  string `json:"name"`
+	ID         string `json:"id"`
+	Login      string `json:"login"`
+	Name       string `json:"name"`
+	DatabaseID int64  `json:"databaseId"`
 }
 
 // Actor is a superset of User and Bot, among others.
diff --git a/go.mod b/go.mod
index 18deafe15..518cae19c 100644
--- a/go.mod
+++ b/go.mod
@@ -49,16 +49,17 @@ require (
 	github.com/sigstore/sigstore-go v1.1.0
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/pflag v1.0.7
-	github.com/stretchr/testify v1.10.0
+	github.com/stretchr/testify v1.11.0
 	github.com/theupdateframework/go-tuf/v2 v2.1.1
+	github.com/vmihailenco/msgpack/v5 v5.4.1
 	github.com/yuin/goldmark v1.7.13
 	github.com/zalando/go-keyring v0.2.6
 	golang.org/x/crypto v0.41.0
 	golang.org/x/sync v0.16.0
 	golang.org/x/term v0.34.0
 	golang.org/x/text v0.28.0
-	google.golang.org/grpc v1.74.2
-	google.golang.org/protobuf v1.36.7
+	google.golang.org/grpc v1.75.0
+	google.golang.org/protobuf v1.36.8
 	gopkg.in/h2non/gock.v1 v1.1.2
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -66,10 +67,10 @@ require (
 require (
 	al.essio.dev/pkg/shellescape v1.6.0 // indirect
 	cel.dev/expr v0.24.0 // indirect
-	cloud.google.com/go v0.121.1 // indirect
-	cloud.google.com/go/auth v0.16.2 // indirect
+	cloud.google.com/go v0.121.4 // indirect
+	cloud.google.com/go/auth v0.16.5 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
-	cloud.google.com/go/compute/metadata v0.7.0 // indirect
+	cloud.google.com/go/compute/metadata v0.8.0 // indirect
 	cloud.google.com/go/iam v1.5.2 // indirect
 	cloud.google.com/go/longrunning v0.6.7 // indirect
 	cloud.google.com/go/monitoring v1.24.2 // indirect
@@ -77,7 +78,7 @@ require (
 	cloud.google.com/go/storage v1.55.0 // indirect
 	dario.cat/mergo v1.0.2 // indirect
 	github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
-	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
@@ -120,12 +121,12 @@ require (
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
 	github.com/gdamore/encoding v1.0.1 // indirect
 	github.com/globocom/go-buffer v1.2.2 // indirect
-	github.com/go-chi/chi v4.1.2+incompatible // indirect
+	github.com/go-chi/chi/v5 v5.2.2 // indirect
 	github.com/go-jose/go-jose/v4 v4.1.1 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/analysis v0.23.0 // indirect
-	github.com/go-openapi/errors v0.22.1 // indirect
+	github.com/go-openapi/errors v0.22.2 // indirect
 	github.com/go-openapi/jsonpointer v0.21.1 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/loads v0.22.0 // indirect
@@ -141,7 +142,7 @@ require (
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
-	github.com/googleapis/gax-go/v2 v2.14.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.15.0 // indirect
 	github.com/gorilla/css v1.0.1 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 // indirect
 	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
@@ -182,11 +183,11 @@ require (
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sagikazarmark/locafero v0.9.0 // indirect
 	github.com/sassoftware/relic v7.2.1+incompatible // indirect
-	github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
+	github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
 	github.com/shibumi/go-pathspec v1.3.0 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
-	github.com/sigstore/rekor v1.3.10 // indirect
+	github.com/sigstore/rekor v1.4.1 // indirect
 	github.com/sigstore/rekor-tiles v0.1.7-0.20250624231741-98cd4a77300f // indirect
 	github.com/sigstore/sigstore v1.9.5 // indirect
 	github.com/sigstore/timestamp-authority v1.2.8 // indirect
@@ -205,6 +206,7 @@ require (
 	github.com/transparency-dev/merkle v0.0.2 // indirect
 	github.com/transparency-dev/tessera v0.2.1-0.20250610150926-8ee4e93b2823 // indirect
 	github.com/vbatts/tar-split v0.12.1 // indirect
+	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/yuin/goldmark-emoji v1.0.6 // indirect
 	github.com/zeebo/errs v1.4.0 // indirect
@@ -216,21 +218,21 @@ require (
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
 	go.opentelemetry.io/otel v1.37.0 // indirect
 	go.opentelemetry.io/otel/metric v1.37.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.36.0 // indirect
-	go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
 	go.opentelemetry.io/otel/trace v1.37.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
-	golang.org/x/mod v0.26.0 // indirect
+	golang.org/x/mod v0.27.0 // indirect
 	golang.org/x/net v0.43.0 // indirect
 	golang.org/x/oauth2 v0.30.0 // indirect
 	golang.org/x/sys v0.35.0 // indirect
 	golang.org/x/time v0.12.0 // indirect
 	golang.org/x/tools v0.35.0 // indirect
-	google.golang.org/api v0.238.0 // indirect
-	google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
+	google.golang.org/api v0.248.0 // indirect
+	google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 )
diff --git a/go.sum b/go.sum
index eeebd6973..7ad0f6745 100644
--- a/go.sum
+++ b/go.sum
@@ -40,8 +40,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
 cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
 cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
-cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw=
-cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
+cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
+cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
 cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
 cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
 cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
@@ -103,8 +103,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
 cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
 cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
 cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
-cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
-cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
+cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
+cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
 cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
@@ -186,8 +186,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
-cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
+cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
+cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
 cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
 cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
 cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@@ -631,24 +631,24 @@ github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjq
 github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM=
 github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
 github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
-github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw=
 github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
@@ -690,34 +690,34 @@ github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIc
 github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
 github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
 github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
-github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
-github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
-github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
-github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
+github.com/aws/aws-sdk-go-v2 v1.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU=
+github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
+github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4=
+github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
-github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc=
-github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
-github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
-github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM=
+github.com/aws/aws-sdk-go-v2/service/kms v1.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg=
+github.com/aws/aws-sdk-go-v2/service/kms v1.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc=
+github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA=
+github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8=
+github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
+github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
@@ -895,8 +895,8 @@ github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JX
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/globocom/go-buffer v1.2.2 h1:ICgtlUe5GIYIZFdAVj57+5WYBR4DA56cX+PYZDhGDwc=
 github.com/globocom/go-buffer v1.2.2/go.mod h1:kY1ALQS0ChiiThmWhsFoT5CYSiuad0t3keIew5LsWdM=
-github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
-github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
+github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
 github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
 github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
@@ -916,8 +916,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
 github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
-github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
-github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
+github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=
+github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
 github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
 github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
 github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -936,8 +936,8 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum
 github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
 github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
 github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
-github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
-github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
@@ -945,8 +945,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
 github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
@@ -1070,14 +1070,16 @@ github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqE
 github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
 github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
 github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
-github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
-github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
+github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
+github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
 github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
+github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
@@ -1092,8 +1094,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
-github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
+github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
 github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
@@ -1146,8 +1148,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
 github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic=
 github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk=
-github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
-github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
+github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
+github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
 github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY=
 github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
@@ -1277,17 +1279,17 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
-github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
-github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
-github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8=
 github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
@@ -1317,8 +1319,8 @@ github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq
 github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4=
 github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k=
 github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
-github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc=
-github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw=
+github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g=
+github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU=
 github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
 github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
 github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
@@ -1331,8 +1333,8 @@ github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZV
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
 github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY=
 github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
-github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU=
-github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A=
+github.com/sigstore/rekor v1.4.1 h1:KK3McuHnptIE9mdNlrc9qh/OVE0AXf4rnScMxJE6xH4=
+github.com/sigstore/rekor v1.4.1/go.mod h1:/McBsz/vrtfi4EInxSIk/MGbDXzgv2+1FQUg1R/uSnE=
 github.com/sigstore/rekor-tiles v0.1.7-0.20250624231741-98cd4a77300f h1:zaqWahYAlVouSm5qwCH+2vZ3eenZFBwzzuBz/IZyy5c=
 github.com/sigstore/rekor-tiles v0.1.7-0.20250624231741-98cd4a77300f/go.mod h1:1Epq0PQ73v5Z276rAY241JyaP8gtD64I6sgYIECHPvc=
 github.com/sigstore/sigstore v1.9.5 h1:Wm1LT9yF4LhQdEMy5A2JeGRHTrAWGjT3ubE5JUSrGVU=
@@ -1343,8 +1345,8 @@ github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.5 h1:qp2VFyKuFQvTGmZwk5Q
 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.5/go.mod h1:DKlQjjr+GsWljEYPycI0Sf8URLCk4EbGA9qYjF47j4g=
 github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.5 h1:CRZcdYn5AOptStsLRAAACudAVmb1qUbhMlzrvm7ju3o=
 github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.5/go.mod h1:b9rFfITq2fp1M3oJmq6lFFhSrAz5vOEJH1qzbMsZWN4=
-github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.5 h1:7U0GsO0UGG1PdtgS6wBkRC0sMgq7BRVaFlPRwN4m1Qg=
-github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.5/go.mod h1:/2qrI0nnCy/DTIPOMFaZlFnNPWEn5UeS70P37XEM88o=
+github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.6-0.20250729224751-181c5d3339b3 h1:a7Yz8C0aBa/LjeiTa9ZLYi9B74GNhFRnUIUdvN6ddVk=
+github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.6-0.20250729224751-181c5d3339b3/go.mod h1:tRtJzSZ48MXJV9bmS8pkb3mP36PCad/Cs+BmVJ3Z4O4=
 github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.5 h1:S2ukEfN1orLKw2wEQIUHDDlzk0YcylhcheeZ5TGk8LI=
 github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.5/go.mod h1:m7sQxVJmDa+rsmS1m6biQxaLX83pzNS7ThUEyjOqkCU=
 github.com/sigstore/timestamp-authority v1.2.8 h1:BEV3fkphwU4zBp3allFAhCqQb99HkiyCXB853RIwuEE=
@@ -1387,8 +1389,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
+github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
@@ -1415,6 +1417,10 @@ github.com/transparency-dev/tessera v0.2.1-0.20250610150926-8ee4e93b2823 h1:s3p7
 github.com/transparency-dev/tessera v0.2.1-0.20250610150926-8ee4e93b2823/go.mod h1:Jv2IDwG1q8QNXZTaI1X6QX8s96WlJn73ka2hT1n4N5c=
 github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
 github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1459,23 +1465,25 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1x
 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
 go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
 go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
-go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
 go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
 go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
-go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU=
-go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU=
+go.step.sm/crypto v0.70.0 h1:Q9Ft7N637mucyZcHZd1+0VVQJVwDCKqcb9CYcYi7cds=
+go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -1552,8 +1560,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
-golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
+golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
+golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1888,6 +1896,8 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
 gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
 gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
 gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
 gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
 gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
@@ -1949,8 +1959,8 @@ google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
 google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
 google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
 google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
-google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs=
-google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
+google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
+google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2090,12 +2100,12 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
 google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
+google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 h1:mVXdvnmR3S3BQOqHECm9NGMjYiRtEvDYcqAqedTXY6s=
+google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2137,8 +2147,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
 google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
-google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
-google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
+google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
+google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -2157,8 +2167,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
-google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
+google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -2226,7 +2236,7 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
 software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
 software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
diff --git a/internal/featuredetection/detector_mock.go b/internal/featuredetection/detector_mock.go
index 6f760f209..2d41c707f 100644
--- a/internal/featuredetection/detector_mock.go
+++ b/internal/featuredetection/detector_mock.go
@@ -20,6 +20,10 @@ func (md *DisabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
 	return gh.ProjectsV1Unsupported
 }
 
+func (md *DisabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
+	return advancedIssueSearchNotSupported, nil
+}
+
 type EnabledDetectorMock struct{}
 
 func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
@@ -37,3 +41,34 @@ func (md *EnabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error)
 func (md *EnabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
 	return gh.ProjectsV1Supported
 }
+
+func (md *EnabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
+	return advancedIssueSearchNotSupported, nil
+}
+
+type AdvancedIssueSearchDetectorMock struct {
+	EnabledDetectorMock
+	searchFeatures SearchFeatures
+}
+
+func (md *AdvancedIssueSearchDetectorMock) SearchFeatures() (SearchFeatures, error) {
+	return md.searchFeatures, nil
+}
+
+func AdvancedIssueSearchUnsupported() *AdvancedIssueSearchDetectorMock {
+	return &AdvancedIssueSearchDetectorMock{
+		searchFeatures: advancedIssueSearchNotSupported,
+	}
+}
+
+func AdvancedIssueSearchSupportedAsOptIn() *AdvancedIssueSearchDetectorMock {
+	return &AdvancedIssueSearchDetectorMock{
+		searchFeatures: advancedIssueSearchSupportedAsOptIn,
+	}
+}
+
+func AdvancedIssueSearchSupportedAsOnlyBackend() *AdvancedIssueSearchDetectorMock {
+	return &AdvancedIssueSearchDetectorMock{
+		searchFeatures: advancedIssueSearchSupportedAsOnlyBackend,
+	}
+}
diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go
index c61f47aeb..786093d93 100644
--- a/internal/featuredetection/feature_detection.go
+++ b/internal/featuredetection/feature_detection.go
@@ -16,6 +16,7 @@ type Detector interface {
 	PullRequestFeatures() (PullRequestFeatures, error)
 	RepositoryFeatures() (RepositoryFeatures, error)
 	ProjectsV1() gh.ProjectsV1Support
+	SearchFeatures() (SearchFeatures, error)
 }
 
 type IssueFeatures struct {
@@ -55,6 +56,43 @@ var allRepositoryFeatures = RepositoryFeatures{
 	AutoMerge:                true,
 }
 
+type SearchFeatures struct {
+	// AdvancedIssueSearch indicates whether the host supports advanced issue
+	// search via API calls.
+	AdvancedIssueSearchAPI bool
+	// AdvancedIssueSearchOptIn indicates whether the host supports advanced
+	// issue search as an opt-in feature, which has to be explicitly enabled in
+	// API calls.
+	AdvancedIssueSearchAPIOptIn bool
+
+	// TODO advancedSearchFuture
+	// When advanced issue search is supported in Pull Requests tab, or in
+	// global search we can introduce more fields to reflect the support status.
+}
+
+// advancedIssueSearchNotSupported mimics GHE <3.18 where advanced issue search
+// is either not supported or is not meant to be used due to not being stable
+// enough (i.e. in preview).
+var advancedIssueSearchNotSupported = SearchFeatures{
+	AdvancedIssueSearchAPI: false,
+}
+
+// advancedIssueSearchSupportedAsOptIn mimics github.com and GHE >=3.18 before
+// the full cleanup of temp types (i.e. ISSUE_ADVANCED search type is still
+// present on the schema).
+var advancedIssueSearchSupportedAsOptIn = SearchFeatures{
+	AdvancedIssueSearchAPI:      true,
+	AdvancedIssueSearchAPIOptIn: true,
+}
+
+// advancedIssueSearchSupportedAsOnlyBackend mimics github.com and GHE >=3.18
+// after the full cleanup of temp types (i.e. ISSUE_ADVANCED search type is
+// removed from the schema).
+var advancedIssueSearchSupportedAsOnlyBackend = SearchFeatures{
+	AdvancedIssueSearchAPI:      true,
+	AdvancedIssueSearchAPIOptIn: false,
+}
+
 type detector struct {
 	host       string
 	httpClient *http.Client
@@ -225,6 +263,101 @@ func (d *detector) ProjectsV1() gh.ProjectsV1Support {
 	return gh.ProjectsV1Unsupported
 }
 
+const (
+	// enterpriseAdvancedIssueSearchSupport is the minimum version of GHES that
+	// supports advanced issue search and gh should use it.
+	//
+	// Note that advanced issue search is also available on GHES 3.17, but it's
+	// at the preview stage and is not as mature as it is on github.com or later
+	// GHES version.
+	enterpriseAdvancedIssueSearchSupport = "3.18.0"
+)
+
+func (d *detector) SearchFeatures() (SearchFeatures, error) {
+	// TODO advancedIssueSearchCleanup
+	// Once GHES 3.17 support ends, we don't need this and, probably, the entire search feature detection.
+
+	// Regarding the release of advanced issue search (AIS, for short), there
+	// are three time spans/periods:
+	//
+	// 1. Pre-deprecation: where both legacy search and AIS are available
+	//    - GraphQL: `ISSUE` and `ISSUE_ADVANCED` search types in GraphQL behave differently
+	//    - REST:    `advance_search=true` query parameter can be used to switch to AIS
+	// 2. Deprecation: only AIS available
+	//    - GraphQL: `ISSUE` and `ISSUE_ADVANCED` search types in GraphQL behave the same (AIS)
+	//    - REST:    `advance_search` query parameter has no effect (AIS)
+	// 3. Cleanup: only AIS available
+	//    - GraphQL: `ISSUE` search type in GraphQL is the only available option (AIS)
+	//    - REST:    `advance_search` query parameter has no effect (AIS)
+	//
+	// Since there's no schema-wise difference between pre-deprecation and
+	// deprecation periods (i.e. `ISSUE_ADVANCED` is available during both),
+	// we cannot figure out the exact time period. The consensus is to to use
+	// the advanced search syntax during both periods.
+
+	var feature SearchFeatures
+
+	if ghauth.IsEnterprise(d.host) {
+		enterpriseAISSupportVersion, err := version.NewVersion(enterpriseAdvancedIssueSearchSupport)
+		if err != nil {
+			return SearchFeatures{}, err
+		}
+
+		hostVersion, err := resolveEnterpriseVersion(d.httpClient, d.host)
+		if err != nil {
+			return SearchFeatures{}, err
+		}
+
+		if hostVersion.GreaterThanOrEqual(enterpriseAISSupportVersion) {
+			// As of August 2025, advanced issue search is going to be available
+			// on GHES 3.18+, including Issues tabs in repositories.
+			feature.AdvancedIssueSearchAPI = true
+
+			// TODO advancedSearchFuture
+			// When the advanced search syntax is supported in global search or
+			// Pull Requests tabs (in repositories), we can add and enable the
+			// corresponding fields.
+		}
+	} else {
+		// As of August 2025, advanced issue search is available on github.com,
+		// including Issues tabs in repositories.
+		feature.AdvancedIssueSearchAPI = true
+
+		// TODO advancedSearchFuture
+		// When the advanced search syntax is supported in global search or
+		// Pull Requests tabs (in repositories), we can add and enable the
+		// corresponding fields.
+	}
+
+	if !feature.AdvancedIssueSearchAPI {
+		return feature, nil
+	}
+
+	var searchTypeFeatureDetection struct {
+		SearchType struct {
+			EnumValues []struct {
+				Name string
+			} `graphql:"enumValues(includeDeprecated: true)"`
+		} `graphql:"SearchType: __type(name: \"SearchType\")"`
+	}
+
+	gql := api.NewClientFromHTTP(d.httpClient)
+	if err := gql.Query(d.host, "SearchType_enumValues", &searchTypeFeatureDetection, nil); err != nil {
+		return SearchFeatures{}, err
+	}
+
+	for _, enumValue := range searchTypeFeatureDetection.SearchType.EnumValues {
+		if enumValue.Name == "ISSUE_ADVANCED" {
+			// As long as ISSUE_ADVANCED is present on the schema, we should
+			// explicitly opt-in when making API calls.
+			feature.AdvancedIssueSearchAPIOptIn = true
+			break
+		}
+	}
+
+	return feature, nil
+}
+
 func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) {
 	var metaResponse struct {
 		InstalledVersion string `json:"installed_version"`
diff --git a/pkg/cmd/agent-task/agent_task.go b/pkg/cmd/agent-task/agent_task.go
new file mode 100644
index 000000000..8b59d59cc
--- /dev/null
+++ b/pkg/cmd/agent-task/agent_task.go
@@ -0,0 +1,100 @@
+package agent
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/MakeNowJust/heredoc"
+	cmdCreate "github.com/cli/cli/v2/pkg/cmd/agent-task/create"
+	cmdList "github.com/cli/cli/v2/pkg/cmd/agent-task/list"
+	cmdView "github.com/cli/cli/v2/pkg/cmd/agent-task/view"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/go-gh/v2/pkg/auth"
+	"github.com/spf13/cobra"
+)
+
+// NewCmdAgentTask creates the base `agent-task` command.
+func NewCmdAgentTask(f *cmdutil.Factory) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "agent-task <command>",
+		Aliases: []string{"agent-tasks", "agent", "agents"},
+		Short:   "Work with agent tasks (preview)",
+		Long: heredoc.Doc(`
+			Working with agent tasks in the GitHub CLI is in preview and
+			subject to change without notice.
+		`),
+		Annotations: map[string]string{
+			"help:arguments": heredoc.Doc(`
+				A task can be identified as argument in any of the following formats:
+				- by pull request number, e.g. "123"; or
+				- by session ID, e.g. "12345abc-12345-12345-12345-12345abc"; or
+				- by URL, e.g. "https://github.com/OWNER/REPO/pull/123/agent-sessions/12345abc-12345-12345-12345-12345abc";
+
+				Identifying tasks by pull request is not recommended for non-interactive use cases as
+				there may be multiple tasks for a given pull request that require disambiguation.
+			`),
+		},
+		Example: heredoc.Doc(`
+			# List your most recent agent tasks
+			$ gh agent-task list
+			
+			# Create a new agent task on the current repository
+			$ gh agent-task create "Improve the performance of the data processing pipeline"
+			
+			# View details about agent tasks associated with a pull request
+			$ gh agent-task view 123
+
+			# View details about a specific agent task
+			$ gh agent-task view 12345abc-12345-12345-12345-12345abc
+		`),
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+			return requireOAuthToken(f)
+		},
+		// This is required to run this root command. We want to
+		// run it to test PersistentPreRunE behavior.
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cmd.Help()
+		},
+	}
+
+	// register subcommands
+	cmd.AddCommand(cmdList.NewCmdList(f, nil))
+	cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
+	cmd.AddCommand(cmdView.NewCmdView(f, nil))
+
+	return cmd
+}
+
+// requireOAuthToken ensures an OAuth (device flow) token is present and valid.
+// agent-task subcommands inherit this check via PersistentPreRunE.
+func requireOAuthToken(f *cmdutil.Factory) error {
+	cfg, err := f.Config()
+	if err != nil {
+		return err
+	}
+
+	authCfg := cfg.Authentication()
+	host, _ := authCfg.DefaultHost()
+	if host == "" {
+		return errors.New("no default host configured; run 'gh auth login'")
+	}
+
+	if auth.IsEnterprise(host) {
+		return errors.New("agent tasks are not supported on this host")
+	}
+
+	token, source := authCfg.ActiveToken(host)
+
+	// Tokens from sources "oauth_token" and "keyring" are likely
+	// minted through our device flow.
+	tokenSourceIsDeviceFlow := source == "oauth_token" || source == "keyring"
+	// Tokens with "gho_" prefix are OAuth tokens.
+	tokenIsOAuth := strings.HasPrefix(token, "gho_")
+
+	// Reject if the token is not from a device flow source or is not an OAuth token
+	if !tokenSourceIsDeviceFlow || !tokenIsOAuth {
+		return fmt.Errorf("this command requires an OAuth token. Re-authenticate with: gh auth login")
+	}
+	return nil
+}
diff --git a/pkg/cmd/agent-task/capi/client.go b/pkg/cmd/agent-task/capi/client.go
new file mode 100644
index 000000000..c25549121
--- /dev/null
+++ b/pkg/cmd/agent-task/capi/client.go
@@ -0,0 +1,72 @@
+package capi
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/cli/cli/v2/internal/gh"
+)
+
+//go:generate moq -rm -out client_mock.go . CapiClient
+
+const baseCAPIURL = "https://api.githubcopilot.com"
+const capiHost = "api.githubcopilot.com"
+
+// CapiClient defines the methods used by the caller. Implementations
+// may be replaced with test doubles in unit tests.
+type CapiClient interface {
+	ListLatestSessionsForViewer(ctx context.Context, limit int) ([]*Session, error)
+	CreateJob(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*Job, error)
+	GetJob(ctx context.Context, owner, repo, jobID string) (*Job, error)
+	GetSession(ctx context.Context, id string) (*Session, error)
+	GetSessionLogs(ctx context.Context, id string) ([]byte, error)
+	ListSessionsByResourceID(ctx context.Context, resourceType string, resourceID int64, limit int) ([]*Session, error)
+	GetPullRequestDatabaseID(ctx context.Context, hostname string, owner string, repo string, number int) (int64, string, error)
+}
+
+// CAPIClient is a client for interacting with the Copilot API
+type CAPIClient struct {
+	httpClient *http.Client
+	authCfg    gh.AuthConfig
+}
+
+// NewCAPIClient creates a new CAPI client. Provide a token and an HTTP client which
+// will be used as the base transport for CAPI requests.
+//
+// The provided HTTP client will be mutated for use with CAPI, so it should not
+// be reused elsewhere.
+func NewCAPIClient(httpClient *http.Client, authCfg gh.AuthConfig) *CAPIClient {
+	host, _ := authCfg.DefaultHost()
+	token, _ := authCfg.ActiveToken(host)
+
+	httpClient.Transport = newCAPITransport(token, httpClient.Transport)
+	return &CAPIClient{
+		httpClient: httpClient,
+		authCfg:    authCfg,
+	}
+}
+
+// capiTransport adds the Copilot auth headers
+type capiTransport struct {
+	rp    http.RoundTripper
+	token string
+}
+
+func newCAPITransport(token string, rp http.RoundTripper) *capiTransport {
+	return &capiTransport{
+		rp:    rp,
+		token: token,
+	}
+}
+
+func (ct *capiTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	req.Header.Set("Authorization", "Bearer "+ct.token)
+
+	// Since this RoundTrip is reused for both Copilot API and
+	// GitHub API requests, we conditionally add the integration
+	// ID only when performing requests to the Copilot API.
+	if req.URL.Host == capiHost {
+		req.Header.Add("Copilot-Integration-Id", "copilot-4-cli")
+	}
+	return ct.rp.RoundTrip(req)
+}
diff --git a/pkg/cmd/agent-task/capi/client_mock.go b/pkg/cmd/agent-task/capi/client_mock.go
new file mode 100644
index 000000000..325a8d513
--- /dev/null
+++ b/pkg/cmd/agent-task/capi/client_mock.go
@@ -0,0 +1,441 @@
+// Code generated by moq; DO NOT EDIT.
+// github.com/matryer/moq
+
+package capi
+
+import (
+	"context"
+	"sync"
+)
+
+// Ensure, that CapiClientMock does implement CapiClient.
+// If this is not the case, regenerate this file with moq.
+var _ CapiClient = &CapiClientMock{}
+
+// CapiClientMock is a mock implementation of CapiClient.
+//
+//	func TestSomethingThatUsesCapiClient(t *testing.T) {
+//
+//		// make and configure a mocked CapiClient
+//		mockedCapiClient := &CapiClientMock{
+//			CreateJobFunc: func(ctx context.Context, owner string, repo string, problemStatement string, baseBranch string) (*Job, error) {
+//				panic("mock out the CreateJob method")
+//			},
+//			GetJobFunc: func(ctx context.Context, owner string, repo string, jobID string) (*Job, error) {
+//				panic("mock out the GetJob method")
+//			},
+//			GetPullRequestDatabaseIDFunc: func(ctx context.Context, hostname string, owner string, repo string, number int) (int64, string, error) {
+//				panic("mock out the GetPullRequestDatabaseID method")
+//			},
+//			GetSessionFunc: func(ctx context.Context, id string) (*Session, error) {
+//				panic("mock out the GetSession method")
+//			},
+//			GetSessionLogsFunc: func(ctx context.Context, id string) ([]byte, error) {
+//				panic("mock out the GetSessionLogs method")
+//			},
+//			ListLatestSessionsForViewerFunc: func(ctx context.Context, limit int) ([]*Session, error) {
+//				panic("mock out the ListLatestSessionsForViewer method")
+//			},
+//			ListSessionsByResourceIDFunc: func(ctx context.Context, resourceType string, resourceID int64, limit int) ([]*Session, error) {
+//				panic("mock out the ListSessionsByResourceID method")
+//			},
+//		}
+//
+//		// use mockedCapiClient in code that requires CapiClient
+//		// and then make assertions.
+//
+//	}
+type CapiClientMock struct {
+	// CreateJobFunc mocks the CreateJob method.
+	CreateJobFunc func(ctx context.Context, owner string, repo string, problemStatement string, baseBranch string) (*Job, error)
+
+	// GetJobFunc mocks the GetJob method.
+	GetJobFunc func(ctx context.Context, owner string, repo string, jobID string) (*Job, error)
+
+	// GetPullRequestDatabaseIDFunc mocks the GetPullRequestDatabaseID method.
+	GetPullRequestDatabaseIDFunc func(ctx context.Context, hostname string, owner string, repo string, number int) (int64, string, error)
+
+	// GetSessionFunc mocks the GetSession method.
+	GetSessionFunc func(ctx context.Context, id string) (*Session, error)
+
+	// GetSessionLogsFunc mocks the GetSessionLogs method.
+	GetSessionLogsFunc func(ctx context.Context, id string) ([]byte, error)
+
+	// ListLatestSessionsForViewerFunc mocks the ListLatestSessionsForViewer method.
+	ListLatestSessionsForViewerFunc func(ctx context.Context, limit int) ([]*Session, error)
+
+	// ListSessionsByResourceIDFunc mocks the ListSessionsByResourceID method.
+	ListSessionsByResourceIDFunc func(ctx context.Context, resourceType string, resourceID int64, limit int) ([]*Session, error)
+
+	// calls tracks calls to the methods.
+	calls struct {
+		// CreateJob holds details about calls to the CreateJob method.
+		CreateJob []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// Owner is the owner argument value.
+			Owner string
+			// Repo is the repo argument value.
+			Repo string
+			// ProblemStatement is the problemStatement argument value.
+			ProblemStatement string
+			// BaseBranch is the baseBranch argument value.
+			BaseBranch string
+		}
+		// GetJob holds details about calls to the GetJob method.
+		GetJob []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// Owner is the owner argument value.
+			Owner string
+			// Repo is the repo argument value.
+			Repo string
+			// JobID is the jobID argument value.
+			JobID string
+		}
+		// GetPullRequestDatabaseID holds details about calls to the GetPullRequestDatabaseID method.
+		GetPullRequestDatabaseID []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// Hostname is the hostname argument value.
+			Hostname string
+			// Owner is the owner argument value.
+			Owner string
+			// Repo is the repo argument value.
+			Repo string
+			// Number is the number argument value.
+			Number int
+		}
+		// GetSession holds details about calls to the GetSession method.
+		GetSession []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// ID is the id argument value.
+			ID string
+		}
+		// GetSessionLogs holds details about calls to the GetSessionLogs method.
+		GetSessionLogs []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// ID is the id argument value.
+			ID string
+		}
+		// ListLatestSessionsForViewer holds details about calls to the ListLatestSessionsForViewer method.
+		ListLatestSessionsForViewer []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// Limit is the limit argument value.
+			Limit int
+		}
+		// ListSessionsByResourceID holds details about calls to the ListSessionsByResourceID method.
+		ListSessionsByResourceID []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// ResourceType is the resourceType argument value.
+			ResourceType string
+			// ResourceID is the resourceID argument value.
+			ResourceID int64
+			// Limit is the limit argument value.
+			Limit int
+		}
+	}
+	lockCreateJob                   sync.RWMutex
+	lockGetJob                      sync.RWMutex
+	lockGetPullRequestDatabaseID    sync.RWMutex
+	lockGetSession                  sync.RWMutex
+	lockGetSessionLogs              sync.RWMutex
+	lockListLatestSessionsForViewer sync.RWMutex
+	lockListSessionsByResourceID    sync.RWMutex
+}
+
+// CreateJob calls CreateJobFunc.
+func (mock *CapiClientMock) CreateJob(ctx context.Context, owner string, repo string, problemStatement string, baseBranch string) (*Job, error) {
+	if mock.CreateJobFunc == nil {
+		panic("CapiClientMock.CreateJobFunc: method is nil but CapiClient.CreateJob was just called")
+	}
+	callInfo := struct {
+		Ctx              context.Context
+		Owner            string
+		Repo             string
+		ProblemStatement string
+		BaseBranch       string
+	}{
+		Ctx:              ctx,
+		Owner:            owner,
+		Repo:             repo,
+		ProblemStatement: problemStatement,
+		BaseBranch:       baseBranch,
+	}
+	mock.lockCreateJob.Lock()
+	mock.calls.CreateJob = append(mock.calls.CreateJob, callInfo)
+	mock.lockCreateJob.Unlock()
+	return mock.CreateJobFunc(ctx, owner, repo, problemStatement, baseBranch)
+}
+
+// CreateJobCalls gets all the calls that were made to CreateJob.
+// Check the length with:
+//
+//	len(mockedCapiClient.CreateJobCalls())
+func (mock *CapiClientMock) CreateJobCalls() []struct {
+	Ctx              context.Context
+	Owner            string
+	Repo             string
+	ProblemStatement string
+	BaseBranch       string
+} {
+	var calls []struct {
+		Ctx              context.Context
+		Owner            string
+		Repo             string
+		ProblemStatement string
+		BaseBranch       string
+	}
+	mock.lockCreateJob.RLock()
+	calls = mock.calls.CreateJob
+	mock.lockCreateJob.RUnlock()
+	return calls
+}
+
+// GetJob calls GetJobFunc.
+func (mock *CapiClientMock) GetJob(ctx context.Context, owner string, repo string, jobID string) (*Job, error) {
+	if mock.GetJobFunc == nil {
+		panic("CapiClientMock.GetJobFunc: method is nil but CapiClient.GetJob was just called")
+	}
+	callInfo := struct {
+		Ctx   context.Context
+		Owner string
+		Repo  string
+		JobID string
+	}{
+		Ctx:   ctx,
+		Owner: owner,
+		Repo:  repo,
+		JobID: jobID,
+	}
+	mock.lockGetJob.Lock()
+	mock.calls.GetJob = append(mock.calls.GetJob, callInfo)
+	mock.lockGetJob.Unlock()
+	return mock.GetJobFunc(ctx, owner, repo, jobID)
+}
+
+// GetJobCalls gets all the calls that were made to GetJob.
+// Check the length with:
+//
+//	len(mockedCapiClient.GetJobCalls())
+func (mock *CapiClientMock) GetJobCalls() []struct {
+	Ctx   context.Context
+	Owner string
+	Repo  string
+	JobID string
+} {
+	var calls []struct {
+		Ctx   context.Context
+		Owner string
+		Repo  string
+		JobID string
+	}
+	mock.lockGetJob.RLock()
+	calls = mock.calls.GetJob
+	mock.lockGetJob.RUnlock()
+	return calls
+}
+
+// GetPullRequestDatabaseID calls GetPullRequestDatabaseIDFunc.
+func (mock *CapiClientMock) GetPullRequestDatabaseID(ctx context.Context, hostname string, owner string, repo string, number int) (int64, string, error) {
+	if mock.GetPullRequestDatabaseIDFunc == nil {
+		panic("CapiClientMock.GetPullRequestDatabaseIDFunc: method is nil but CapiClient.GetPullRequestDatabaseID was just called")
+	}
+	callInfo := struct {
+		Ctx      context.Context
+		Hostname string
+		Owner    string
+		Repo     string
+		Number   int
+	}{
+		Ctx:      ctx,
+		Hostname: hostname,
+		Owner:    owner,
+		Repo:     repo,
+		Number:   number,
+	}
+	mock.lockGetPullRequestDatabaseID.Lock()
+	mock.calls.GetPullRequestDatabaseID = append(mock.calls.GetPullRequestDatabaseID, callInfo)
+	mock.lockGetPullRequestDatabaseID.Unlock()
+	return mock.GetPullRequestDatabaseIDFunc(ctx, hostname, owner, repo, number)
+}
+
+// GetPullRequestDatabaseIDCalls gets all the calls that were made to GetPullRequestDatabaseID.
+// Check the length with:
+//
+//	len(mockedCapiClient.GetPullRequestDatabaseIDCalls())
+func (mock *CapiClientMock) GetPullRequestDatabaseIDCalls() []struct {
+	Ctx      context.Context
+	Hostname string
+	Owner    string
+	Repo     string
+	Number   int
+} {
+	var calls []struct {
+		Ctx      context.Context
+		Hostname string
+		Owner    string
+		Repo     string
+		Number   int
+	}
+	mock.lockGetPullRequestDatabaseID.RLock()
+	calls = mock.calls.GetPullRequestDatabaseID
+	mock.lockGetPullRequestDatabaseID.RUnlock()
+	return calls
+}
+
+// GetSession calls GetSessionFunc.
+func (mock *CapiClientMock) GetSession(ctx context.Context, id string) (*Session, error) {
+	if mock.GetSessionFunc == nil {
+		panic("CapiClientMock.GetSessionFunc: method is nil but CapiClient.GetSession was just called")
+	}
+	callInfo := struct {
+		Ctx context.Context
+		ID  string
+	}{
+		Ctx: ctx,
+		ID:  id,
+	}
+	mock.lockGetSession.Lock()
+	mock.calls.GetSession = append(mock.calls.GetSession, callInfo)
+	mock.lockGetSession.Unlock()
+	return mock.GetSessionFunc(ctx, id)
+}
+
+// GetSessionCalls gets all the calls that were made to GetSession.
+// Check the length with:
+//
+//	len(mockedCapiClient.GetSessionCalls())
+func (mock *CapiClientMock) GetSessionCalls() []struct {
+	Ctx context.Context
+	ID  string
+} {
+	var calls []struct {
+		Ctx context.Context
+		ID  string
+	}
+	mock.lockGetSession.RLock()
+	calls = mock.calls.GetSession
+	mock.lockGetSession.RUnlock()
+	return calls
+}
+
+// GetSessionLogs calls GetSessionLogsFunc.
+func (mock *CapiClientMock) GetSessionLogs(ctx context.Context, id string) ([]byte, error) {
+	if mock.GetSessionLogsFunc == nil {
+		panic("CapiClientMock.GetSessionLogsFunc: method is nil but CapiClient.GetSessionLogs was just called")
+	}
+	callInfo := struct {
+		Ctx context.Context
+		ID  string
+	}{
+		Ctx: ctx,
+		ID:  id,
+	}
+	mock.lockGetSessionLogs.Lock()
+	mock.calls.GetSessionLogs = append(mock.calls.GetSessionLogs, callInfo)
+	mock.lockGetSessionLogs.Unlock()
+	return mock.GetSessionLogsFunc(ctx, id)
+}
+
+// GetSessionLogsCalls gets all the calls that were made to GetSessionLogs.
+// Check the length with:
+//
+//	len(mockedCapiClient.GetSessionLogsCalls())
+func (mock *CapiClientMock) GetSessionLogsCalls() []struct {
+	Ctx context.Context
+	ID  string
+} {
+	var calls []struct {
+		Ctx context.Context
+		ID  string
+	}
+	mock.lockGetSessionLogs.RLock()
+	calls = mock.calls.GetSessionLogs
+	mock.lockGetSessionLogs.RUnlock()
+	return calls
+}
+
+// ListLatestSessionsForViewer calls ListLatestSessionsForViewerFunc.
+func (mock *CapiClientMock) ListLatestSessionsForViewer(ctx context.Context, limit int) ([]*Session, error) {
+	if mock.ListLatestSessionsForViewerFunc == nil {
+		panic("CapiClientMock.ListLatestSessionsForViewerFunc: method is nil but CapiClient.ListLatestSessionsForViewer was just called")
+	}
+	callInfo := struct {
+		Ctx   context.Context
+		Limit int
+	}{
+		Ctx:   ctx,
+		Limit: limit,
+	}
+	mock.lockListLatestSessionsForViewer.Lock()
+	mock.calls.ListLatestSessionsForViewer = append(mock.calls.ListLatestSessionsForViewer, callInfo)
+	mock.lockListLatestSessionsForViewer.Unlock()
+	return mock.ListLatestSessionsForViewerFunc(ctx, limit)
+}
+
+// ListLatestSessionsForViewerCalls gets all the calls that were made to ListLatestSessionsForViewer.
+// Check the length with:
+//
+//	len(mockedCapiClient.ListLatestSessionsForViewerCalls())
+func (mock *CapiClientMock) ListLatestSessionsForViewerCalls() []struct {
+	Ctx   context.Context
+	Limit int
+} {
+	var calls []struct {
+		Ctx   context.Context
+		Limit int
+	}
+	mock.lockListLatestSessionsForViewer.RLock()
+	calls = mock.calls.ListLatestSessionsForViewer
+	mock.lockListLatestSessionsForViewer.RUnlock()
+	return calls
+}
+
+// ListSessionsByResourceID calls ListSessionsByResourceIDFunc.
+func (mock *CapiClientMock) ListSessionsByResourceID(ctx context.Context, resourceType string, resourceID int64, limit int) ([]*Session, error) {
+	if mock.ListSessionsByResourceIDFunc == nil {
+		panic("CapiClientMock.ListSessionsByResourceIDFunc: method is nil but CapiClient.ListSessionsByResourceID was just called")
+	}
+	callInfo := struct {
+		Ctx          context.Context
+		ResourceType string
+		ResourceID   int64
+		Limit        int
+	}{
+		Ctx:          ctx,
+		ResourceType: resourceType,
+		ResourceID:   resourceID,
+		Limit:        limit,
+	}
+	mock.lockListSessionsByResourceID.Lock()
+	mock.calls.ListSessionsByResourceID = append(mock.calls.ListSessionsByResourceID, callInfo)
+	mock.lockListSessionsByResourceID.Unlock()
+	return mock.ListSessionsByResourceIDFunc(ctx, resourceType, resourceID, limit)
+}
+
+// ListSessionsByResourceIDCalls gets all the calls that were made to ListSessionsByResourceID.
+// Check the length with:
+//
+//	len(mockedCapiClient.ListSessionsByResourceIDCalls())
+func (mock *CapiClientMock) ListSessionsByResourceIDCalls() []struct {
+	Ctx          context.Context
+	ResourceType string
+	ResourceID   int64
+	Limit        int
+} {
+	var calls []struct {
+		Ctx          context.Context
+		ResourceType string
+		ResourceID   int64
+		Limit        int
+	}
+	mock.lockListSessionsByResourceID.RLock()
+	calls = mock.calls.ListSessionsByResourceID
+	mock.lockListSessionsByResourceID.RUnlock()
+	return calls
+}
diff --git a/pkg/cmd/agent-task/capi/job.go b/pkg/cmd/agent-task/capi/job.go
new file mode 100644
index 000000000..5a56323cb
--- /dev/null
+++ b/pkg/cmd/agent-task/capi/job.go
@@ -0,0 +1,137 @@
+package capi
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+const defaultEventType = "gh_cli"
+
+// Job represents a coding agent's task. Used to request a new session.
+type Job struct {
+	ID                string          `json:"job_id,omitempty"`
+	SessionID         string          `json:"session_id,omitempty"`
+	ProblemStatement  string          `json:"problem_statement,omitempty"`
+	EventType         string          `json:"event_type,omitempty"`
+	ContentFilterMode string          `json:"content_filter_mode,omitempty"`
+	Status            string          `json:"status,omitempty"`
+	Result            string          `json:"result,omitempty"`
+	Actor             *JobActor       `json:"actor,omitempty"`
+	CreatedAt         time.Time       `json:"created_at,omitempty"`
+	UpdatedAt         time.Time       `json:"updated_at,omitempty"`
+	PullRequest       *JobPullRequest `json:"pull_request,omitempty"`
+	WorkflowRun       *struct {
+		ID string `json:"id"`
+	} `json:"workflow_run,omitempty"`
+	ErrorInfo *JobError `json:"error,omitempty"`
+}
+
+type JobActor struct {
+	ID    int    `json:"id"`
+	Login string `json:"login"`
+}
+
+type JobPullRequest struct {
+	ID      int    `json:"id"`
+	Number  int    `json:"number"`
+	BaseRef string `json:"base_ref,omitempty"`
+}
+
+type JobError struct {
+	Message            string `json:"message"`
+	ResponseStatusCode int    `json:"response_status_code,string"`
+	Service            string `json:"service"`
+}
+
+const jobsBasePathV1 = baseCAPIURL + "/agents/swe/v1/jobs"
+
+// CreateJob queues a new job using the v1 Jobs API. It may or may not
+// return Pull Request information. If Pull Request information is required
+// following up by polling GetJob with the job ID is necessary.
+func (c *CAPIClient) CreateJob(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*Job, error) {
+	if owner == "" || repo == "" {
+		return nil, errors.New("owner and repo are required")
+	}
+	if problemStatement == "" {
+		return nil, errors.New("problem statement is required")
+	}
+
+	url := fmt.Sprintf("%s/%s/%s", jobsBasePathV1, url.PathEscape(owner), url.PathEscape(repo))
+
+	prOpts := JobPullRequest{}
+	if baseBranch != "" {
+		prOpts.BaseRef = "refs/heads/" + baseBranch
+	}
+
+	payload := &Job{
+		ProblemStatement: problemStatement,
+		EventType:        defaultEventType,
+		PullRequest:      &prOpts,
+	}
+
+	b, _ := json.Marshal(payload)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	res, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+
+	var j Job
+	if err := json.NewDecoder(res.Body).Decode(&j); err != nil {
+		if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK { // accept 201 or 200
+			// This happens when there's an error like unauthorized (401).
+			statusText := fmt.Sprintf("%d %s", res.StatusCode, http.StatusText(res.StatusCode))
+			return nil, fmt.Errorf("failed to create job: %s", statusText)
+		}
+		return nil, fmt.Errorf("failed to decode create job response: %w", err)
+	}
+
+	if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK { // accept 201 or 200
+		if j.ErrorInfo != nil {
+			return nil, fmt.Errorf("failed to create job: %s", j.ErrorInfo.Message)
+		}
+		statusText := fmt.Sprintf("%d %s", res.StatusCode, http.StatusText(res.StatusCode))
+		return nil, fmt.Errorf("failed to create job: %s", statusText)
+	}
+
+	return &j, nil
+}
+
+// GetJob retrieves a agent job
+func (c *CAPIClient) GetJob(ctx context.Context, owner, repo, jobID string) (*Job, error) {
+	if owner == "" || repo == "" || jobID == "" {
+		return nil, errors.New("owner, repo, and jobID are required")
+	}
+	url := fmt.Sprintf("%s/%s/%s/%s", jobsBasePathV1, url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(jobID))
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+	res, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	if res.StatusCode != http.StatusOK {
+		// Normalize to "<code> <text>" form
+		statusText := fmt.Sprintf("%d %s", res.StatusCode, http.StatusText(res.StatusCode))
+		return nil, fmt.Errorf("failed to get job: %s", statusText)
+	}
+	var j Job
+	if err := json.NewDecoder(res.Body).Decode(&j); err != nil {
+		return nil, fmt.Errorf("failed to decode get job response: %w", err)
+	}
+	return &j, nil
+}
diff --git a/pkg/cmd/agent-task/capi/sessions.go b/pkg/cmd/agent-task/capi/sessions.go
new file mode 100644
index 000000000..c147afa9c
--- /dev/null
+++ b/pkg/cmd/agent-task/capi/sessions.go
@@ -0,0 +1,511 @@
+package capi
+
+import (
+	"bytes"
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"math"
+	"net/http"
+	"net/url"
+	"slices"
+	"strconv"
+	"time"
+
+	"github.com/cli/cli/v2/api"
+	"github.com/shurcooL/githubv4"
+	"github.com/vmihailenco/msgpack/v5"
+)
+
+const AgentsHomeURL = "https://github.com/copilot/agents"
+
+var defaultSessionsPerPage = 50
+
+var ErrSessionNotFound = errors.New("not found")
+
+// session is an in-flight agent task
+type session struct {
+	ID               string    `json:"id"`
+	Name             string    `json:"name"`
+	UserID           int64     `json:"user_id"`
+	AgentID          int64     `json:"agent_id"`
+	Logs             string    `json:"logs"`
+	State            string    `json:"state"`
+	OwnerID          uint64    `json:"owner_id"`
+	RepoID           uint64    `json:"repo_id"`
+	ResourceType     string    `json:"resource_type"`
+	ResourceID       int64     `json:"resource_id"`
+	ResourceGlobalID string    `json:"resource_global_id"`
+	LastUpdatedAt    time.Time `json:"last_updated_at,omitempty"`
+	CreatedAt        time.Time `json:"created_at,omitempty"`
+	CompletedAt      time.Time `json:"completed_at,omitempty"`
+	EventURL         string    `json:"event_url"`
+	EventType        string    `json:"event_type"`
+	PremiumRequests  float64   `json:"premium_requests"`
+	WorkflowRunID    uint64    `json:"workflow_run_id,omitempty"`
+	Error            *struct {
+		Code    string `json:"code"`
+		Message string `json:"message"`
+	} `json:"error,omitempty"`
+}
+
+// A shim of a full pull request because looking up by node ID
+// using the full api.PullRequest type fails on unions (actors)
+type sessionPullRequest struct {
+	ID             string
+	FullDatabaseID string
+	Number         int
+	Title          string
+	State          string
+	URL            string
+	Body           string
+	IsDraft        bool
+
+	CreatedAt time.Time
+	UpdatedAt time.Time
+	ClosedAt  *time.Time
+	MergedAt  *time.Time
+
+	Repository *api.PRRepository
+}
+
+// Session is a hydrated in-flight agent task
+type Session struct {
+	ID              string
+	Name            string
+	UserID          int64
+	AgentID         int64
+	Logs            string
+	State           string
+	OwnerID         uint64
+	RepoID          uint64
+	ResourceType    string
+	ResourceID      int64
+	LastUpdatedAt   time.Time
+	CreatedAt       time.Time
+	CompletedAt     time.Time
+	EventURL        string
+	EventType       string
+	PremiumRequests float64
+	WorkflowRunID   uint64
+	Error           *SessionError
+
+	PullRequest *api.PullRequest
+	User        *api.GitHubUser
+}
+
+type SessionError struct {
+	Code    string
+	Message string
+}
+
+type resource struct {
+	ID                   string            `json:"id"`
+	UserID               uint64            `json:"user_id"`
+	ResourceType         string            `json:"resource_type"`
+	ResourceID           int64             `json:"resource_id"`
+	ResourceGlobalID     string            `json:"resource_global_id"`
+	SessionCount         int               `json:"session_count"`
+	SessionLastUpdatedAt int64             `json:"last_updated_at"`
+	SessionState         string            `json:"state,omitempty"`
+	ResourceState        string            `json:"resource_state"`
+	Sessions             []resourceSession `json:"sessions"`
+}
+
+type resourceSession struct {
+	SessionID            string `json:"id"`
+	Name                 string `json:"name"`
+	SessionState         string `json:"state,omitempty"`
+	SessionLastUpdatedAt int64  `json:"last_updated_at"`
+}
+
+// ListLatestSessionsForViewer lists all agent sessions for the
+// authenticated user up to limit.
+func (c *CAPIClient) ListLatestSessionsForViewer(ctx context.Context, limit int) ([]*Session, error) {
+	if limit == 0 {
+		return nil, nil
+	}
+
+	url := baseCAPIURL + "/agents/sessions"
+	pageSize := defaultSessionsPerPage
+
+	seenResources := make(map[int64]struct{})
+	latestSessions := make([]session, 0, limit)
+	for page := 1; ; page++ {
+		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
+		if err != nil {
+			return nil, err
+		}
+
+		q := req.URL.Query()
+		q.Set("page_size", strconv.Itoa(pageSize))
+		q.Set("page_number", strconv.Itoa(page))
+		q.Set("sort", "last_updated_at,desc")
+		req.URL.RawQuery = q.Encode()
+
+		res, err := c.httpClient.Do(req)
+		if err != nil {
+			return nil, err
+		}
+		defer res.Body.Close()
+		if res.StatusCode != http.StatusOK {
+			return nil, fmt.Errorf("failed to list sessions: %s", res.Status)
+		}
+		var response struct {
+			Sessions []session `json:"sessions"`
+		}
+		if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
+			return nil, fmt.Errorf("failed to decode sessions response: %w", err)
+		}
+
+		// Process only the newly fetched page worth of sessions.
+		pageSessions := response.Sessions
+
+		// De-duplicate sessions by resource ID.
+		// Because the API returns newest first, once we've seen
+		// a resource ID we can ignore any older sessions for it.
+		for _, s := range pageSessions {
+			if _, exists := seenResources[s.ResourceID]; exists {
+				continue
+			}
+
+			// A zero resource ID is a temporary situation before a PR/resource
+			// is associated with the session. We should not mark such case as seen.
+			if s.ResourceID != 0 {
+				seenResources[s.ResourceID] = struct{}{}
+			}
+
+			latestSessions = append(latestSessions, s)
+			if len(latestSessions) >= limit {
+				break
+			}
+		}
+
+		if len(response.Sessions) < pageSize || len(latestSessions) >= limit {
+			break
+		}
+	}
+
+	// Drop any above the limit
+	if len(latestSessions) > limit {
+		latestSessions = latestSessions[:limit]
+	}
+
+	result, err := c.hydrateSessionPullRequestsAndUsers(latestSessions)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch session resources: %w", err)
+	}
+
+	return result, nil
+}
+
+// GetSession retrieves a specific agent session by ID.
+func (c *CAPIClient) GetSession(ctx context.Context, id string) (*Session, error) {
+	if id == "" {
+		return nil, fmt.Errorf("missing session ID")
+	}
+
+	url := fmt.Sprintf("%s/agents/sessions/%s", baseCAPIURL, url.PathEscape(id))
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+
+	res, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	defer res.Body.Close()
+	if res.StatusCode != http.StatusOK {
+		if res.StatusCode == http.StatusNotFound {
+			return nil, ErrSessionNotFound
+		}
+		return nil, fmt.Errorf("failed to get session: %s", res.Status)
+	}
+
+	var rawSession session
+	if err := json.NewDecoder(res.Body).Decode(&rawSession); err != nil {
+		return nil, fmt.Errorf("failed to decode session response: %w", err)
+	}
+
+	sessions, err := c.hydrateSessionPullRequestsAndUsers([]session{rawSession})
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch session resources: %w", err)
+	}
+
+	return sessions[0], nil
+}
+
+// GetSessionLogs retrieves logs of an agent session identified by ID.
+func (c *CAPIClient) GetSessionLogs(ctx context.Context, id string) ([]byte, error) {
+	if id == "" {
+		return nil, fmt.Errorf("missing session ID")
+	}
+
+	url := fmt.Sprintf("%s/agents/sessions/%s/logs", baseCAPIURL, url.PathEscape(id))
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+
+	res, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	defer res.Body.Close()
+	if res.StatusCode != http.StatusOK {
+		if res.StatusCode == http.StatusNotFound {
+			return nil, ErrSessionNotFound
+		}
+		return nil, fmt.Errorf("failed to get session: %s", res.Status)
+	}
+
+	return io.ReadAll(res.Body)
+}
+
+// ListSessionsByResourceID retrieves sessions associated with the given resource type and ID.
+func (c *CAPIClient) ListSessionsByResourceID(ctx context.Context, resourceType string, resourceID int64, limit int) ([]*Session, error) {
+	if resourceType == "" || resourceID == 0 {
+		return nil, fmt.Errorf("missing resource type/ID")
+	}
+
+	if limit == 0 {
+		return nil, nil
+	}
+
+	url := fmt.Sprintf("%s/agents/resource/%s/%d", baseCAPIURL, url.PathEscape(resourceType), resourceID)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+
+	res, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	if res.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("failed to list sessions: %s", res.Status)
+	}
+
+	var response resource
+	if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
+		return nil, fmt.Errorf("failed to decode sessions response: %w", err)
+	}
+
+	sessions := make([]session, 0, len(response.Sessions))
+	for _, s := range response.Sessions {
+		session := session{
+			ID:               s.SessionID,
+			Name:             s.Name,
+			UserID:           int64(response.UserID),
+			ResourceType:     response.ResourceType,
+			ResourceID:       response.ResourceID,
+			ResourceGlobalID: response.ResourceGlobalID,
+			State:            s.SessionState,
+		}
+		if s.SessionLastUpdatedAt != 0 {
+			session.LastUpdatedAt = time.Unix(s.SessionLastUpdatedAt, 0).UTC()
+		}
+		sessions = append(sessions, session)
+	}
+
+	result, err := c.hydrateSessionPullRequestsAndUsers(sessions)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch session resources: %w", err)
+	}
+	return result, nil
+}
+
+// hydrateSessionPullRequestsAndUsers hydrates pull request and user information in sessions
+func (c *CAPIClient) hydrateSessionPullRequestsAndUsers(sessions []session) ([]*Session, error) {
+	if len(sessions) == 0 {
+		return nil, nil
+	}
+
+	prNodeIds := make([]string, 0, len(sessions))
+	userNodeIds := make([]string, 0, len(sessions))
+	for _, session := range sessions {
+		if session.ResourceType == "pull" {
+			prNodeID := session.ResourceGlobalID
+			// TODO: probably this can be dropped since the API should always
+			// keep returning the resource global ID.
+			if session.ResourceGlobalID == "" {
+				prNodeID = generatePullRequestNodeID(int64(session.RepoID), session.ResourceID)
+			}
+			if !slices.Contains(prNodeIds, prNodeID) {
+				prNodeIds = append(prNodeIds, prNodeID)
+			}
+		}
+
+		userNodeId := generateUserNodeID(session.UserID)
+		if !slices.Contains(userNodeIds, userNodeId) {
+			userNodeIds = append(userNodeIds, userNodeId)
+		}
+	}
+	apiClient := api.NewClientFromHTTP(c.httpClient)
+
+	var resp struct {
+		Nodes []struct {
+			TypeName    string             `graphql:"__typename"`
+			PullRequest sessionPullRequest `graphql:"... on PullRequest"`
+			User        api.GitHubUser     `graphql:"... on User"`
+		} `graphql:"nodes(ids: $ids)"`
+	}
+
+	ids := make([]string, 0, len(prNodeIds)+len(userNodeIds))
+	ids = append(ids, prNodeIds...)
+	ids = append(ids, userNodeIds...)
+
+	// TODO handle pagination
+	host, _ := c.authCfg.DefaultHost()
+	err := apiClient.Query(host, "FetchPRsAndUsersForAgentTaskSessions", &resp, map[string]any{
+		"ids": ids,
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	prMap := make(map[string]*api.PullRequest, len(prNodeIds))
+	userMap := make(map[int64]*api.GitHubUser, len(userNodeIds))
+	for _, node := range resp.Nodes {
+		switch node.TypeName {
+		case "User":
+			userMap[node.User.DatabaseID] = &node.User
+		case "PullRequest":
+			prMap[node.PullRequest.FullDatabaseID] = &api.PullRequest{
+				ID:             node.PullRequest.ID,
+				FullDatabaseID: node.PullRequest.FullDatabaseID,
+				Number:         node.PullRequest.Number,
+				Title:          node.PullRequest.Title,
+				State:          node.PullRequest.State,
+				IsDraft:        node.PullRequest.IsDraft,
+				URL:            node.PullRequest.URL,
+				Body:           node.PullRequest.Body,
+				CreatedAt:      node.PullRequest.CreatedAt,
+				UpdatedAt:      node.PullRequest.UpdatedAt,
+				ClosedAt:       node.PullRequest.ClosedAt,
+				MergedAt:       node.PullRequest.MergedAt,
+				Repository:     node.PullRequest.Repository,
+			}
+		}
+	}
+
+	newSessions := make([]*Session, 0, len(sessions))
+	for _, s := range sessions {
+		newSession := fromAPISession(s)
+		newSession.PullRequest = prMap[strconv.FormatInt(s.ResourceID, 10)]
+		newSession.User = userMap[s.UserID]
+		newSessions = append(newSessions, newSession)
+	}
+
+	return newSessions, nil
+}
+
+// GetPullRequestDatabaseID retrieves the database ID and URL of a pull request given its number in a repository.
+func (c *CAPIClient) GetPullRequestDatabaseID(ctx context.Context, hostname string, owner string, repo string, number int) (int64, string, error) {
+	// TODO: better int handling so we don't need to do bounds checks
+	// to both ensure a panic is impossible and that we do not trigger
+	// CodeQL alerts.
+	if number <= 0 || number > math.MaxInt32 {
+		return 0, "", fmt.Errorf("pull request number %d out of bounds", number)
+	}
+
+	var resp struct {
+		Repository struct {
+			PullRequest struct {
+				FullDatabaseID string `graphql:"fullDatabaseId"`
+				URL            string `graphql:"url"`
+			} `graphql:"pullRequest(number: $number)"`
+		} `graphql:"repository(owner: $owner, name: $repo)"`
+	}
+
+	variables := map[string]interface{}{
+		"owner":  githubv4.String(owner),
+		"repo":   githubv4.String(repo),
+		"number": githubv4.Int(number),
+	}
+
+	apiClient := api.NewClientFromHTTP(c.httpClient)
+	if err := apiClient.Query(hostname, "GetPullRequestFullDatabaseID", &resp, variables); err != nil {
+		return 0, "", err
+	}
+
+	databaseID, err := strconv.ParseInt(resp.Repository.PullRequest.FullDatabaseID, 10, 64)
+	if err != nil {
+		return 0, "", err
+	}
+	return databaseID, resp.Repository.PullRequest.URL, nil
+}
+
+// generatePullRequestNodeID converts an int64 databaseID and repoID to a GraphQL Node ID format
+// with the "PR_" prefix for pull requests
+func generatePullRequestNodeID(repoID, pullRequestID int64) string {
+	buf := bytes.Buffer{}
+	parts := []int64{0, repoID, pullRequestID}
+
+	encoder := msgpack.NewEncoder(&buf)
+	encoder.UseCompactInts(true)
+
+	if err := encoder.Encode(parts); err != nil {
+		panic(err)
+	}
+
+	encoded := base64.RawURLEncoding.EncodeToString(buf.Bytes())
+
+	return "PR_" + encoded
+}
+
+func generateUserNodeID(userID int64) string {
+	buf := bytes.Buffer{}
+	parts := []int64{0, userID}
+
+	encoder := msgpack.NewEncoder(&buf)
+	encoder.UseCompactInts(true)
+
+	if err := encoder.Encode(parts); err != nil {
+		panic(err)
+	}
+
+	encoded := base64.RawURLEncoding.EncodeToString(buf.Bytes())
+
+	return "U_" + encoded
+}
+
+func fromAPISession(s session) *Session {
+	result := Session{
+		ID:              s.ID,
+		Name:            s.Name,
+		UserID:          s.UserID,
+		AgentID:         s.AgentID,
+		Logs:            s.Logs,
+		State:           s.State,
+		OwnerID:         s.OwnerID,
+		RepoID:          s.RepoID,
+		ResourceType:    s.ResourceType,
+		ResourceID:      s.ResourceID,
+		LastUpdatedAt:   s.LastUpdatedAt,
+		CreatedAt:       s.CreatedAt,
+		CompletedAt:     s.CompletedAt,
+		EventURL:        s.EventURL,
+		EventType:       s.EventType,
+		PremiumRequests: s.PremiumRequests,
+		WorkflowRunID:   s.WorkflowRunID,
+	}
+	if s.Error != nil {
+		result.Error = &SessionError{
+			Code:    s.Error.Code,
+			Message: s.Error.Message,
+		}
+	}
+	return &result
+}
diff --git a/pkg/cmd/agent-task/create/create.go b/pkg/cmd/agent-task/create/create.go
new file mode 100644
index 000000000..9c9b8ed68
--- /dev/null
+++ b/pkg/cmd/agent-task/create/create.go
@@ -0,0 +1,280 @@
+package create
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/cenkalti/backoff/v4"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/internal/gh"
+	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/spf13/cobra"
+)
+
+const defaultLogPollInterval = 5 * time.Second
+
+// CreateOptions holds options for create command
+type CreateOptions struct {
+	IO         *iostreams.IOStreams
+	BaseRepo   func() (ghrepo.Interface, error)
+	CapiClient func() (capi.CapiClient, error)
+	Config     func() (gh.Config, error)
+
+	LogRenderer func() shared.LogRenderer
+	Sleep       func(d time.Duration)
+
+	ProblemStatement     string
+	BackOff              backoff.BackOff
+	BaseBranch           string
+	Prompter             prompter.Prompter
+	ProblemStatementFile string
+	Follow               bool
+}
+
+func defaultLogRenderer() shared.LogRenderer {
+	return shared.NewLogRenderer()
+}
+
+func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
+	opts := &CreateOptions{
+		IO:          f.IOStreams,
+		CapiClient:  shared.CapiClientFunc(f),
+		Config:      f.Config,
+		Prompter:    f.Prompter,
+		LogRenderer: defaultLogRenderer,
+		Sleep:       time.Sleep,
+	}
+
+	cmd := &cobra.Command{
+		Use:   "create [<task description>] [flags]",
+		Short: "Create an agent task (preview)",
+		Args:  cobra.MaximumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			// Support -R/--repo override
+			opts.BaseRepo = f.BaseRepo
+
+			if err := cmdutil.MutuallyExclusive("only one of -F or arg can be provided", len(args) > 0, opts.ProblemStatementFile != ""); err != nil {
+				return err
+			}
+
+			// Populate ProblemStatement from arg
+			if len(args) > 0 {
+				opts.ProblemStatement = args[0]
+				if strings.TrimSpace(opts.ProblemStatement) == "" {
+					return cmdutil.FlagErrorf("task description cannot be empty")
+				}
+			} else if opts.ProblemStatementFile == "" && !opts.IO.CanPrompt() {
+				return cmdutil.FlagErrorf("a task description or -F is required when running non-interactively")
+			}
+
+			if runF != nil {
+				return runF(opts)
+			}
+			return createRun(opts)
+		},
+		Example: heredoc.Doc(`
+			# Create a task from an inline description
+			$ gh agent-task create "build me a new app"
+
+			# Create a task from an inline description and follow logs
+			$ gh agent-task create "build me a new app" --follow
+
+			# Create a task from a file
+			$ gh agent-task create -F task-desc.md
+
+			# Create a task with problem statement from stdin
+			$ echo "build me a new app" | gh agent-task create -F -
+
+			# Create a task with an editor
+			$ gh agent-task create
+
+			# Create a task with an editor and a file as a template
+			$ gh agent-task create -F task-desc.md
+
+			# Select a different base branch for the PR
+			$ gh agent-task create "fix errors" --base branch
+		`),
+	}
+
+	cmdutil.EnableRepoOverride(cmd, f)
+
+	cmd.Flags().StringVarP(&opts.ProblemStatementFile, "from-file", "F", "", "Read task description from `file` (use \"-\" to read from standard input)")
+	cmd.Flags().StringVarP(&opts.BaseBranch, "base", "b", "", "Base branch for the pull request (use default branch if not provided)")
+	cmd.Flags().BoolVar(&opts.Follow, "follow", false, "Follow agent session logs")
+
+	return cmd
+}
+
+func createRun(opts *CreateOptions) error {
+	repo, err := opts.BaseRepo()
+	if err != nil || repo == nil {
+		// Not printing the error that came back from BaseRepo() here because we want
+		// something clear, human friendly, and actionable.
+		return fmt.Errorf("a repository is required; re-run in a repository or supply one with --repo owner/name")
+	}
+
+	if opts.ProblemStatement == "" {
+		if opts.ProblemStatementFile != "" {
+			fileContent, err := cmdutil.ReadFile(opts.ProblemStatementFile, opts.IO.In)
+			if err != nil {
+				return fmt.Errorf("could not read task description file: %w", err)
+			}
+
+			trimmed := strings.TrimSpace(string(fileContent))
+			if trimmed == "" {
+				return errors.New("task description file cannot be empty")
+			}
+
+			opts.ProblemStatement = trimmed
+		} else {
+			desc, err := opts.Prompter.MarkdownEditor("Enter the task description", opts.ProblemStatement, false)
+			if err != nil {
+				return err
+			}
+
+			trimmed := strings.TrimSpace(string(desc))
+			if trimmed == "" {
+				return errors.New("a task description is required")
+			}
+
+			opts.ProblemStatement = trimmed
+		}
+	}
+
+	client, err := opts.CapiClient()
+	if err != nil {
+		return err
+	}
+
+	ctx := context.Background()
+	opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Creating agent task in %s/%s...", repo.RepoOwner(), repo.RepoName()))
+	defer opts.IO.StopProgressIndicator()
+
+	job, err := client.CreateJob(ctx, repo.RepoOwner(), repo.RepoName(), opts.ProblemStatement, opts.BaseBranch)
+	if err != nil {
+		return err
+	}
+
+	if opts.Follow {
+		return followLogs(opts, client, job.SessionID)
+	}
+
+	sessionURL, err := fetchJobSessionURL(ctx, client, repo, job, opts.BackOff)
+	opts.IO.StopProgressIndicator()
+
+	if sessionURL != "" {
+		fmt.Fprintln(opts.IO.Out, sessionURL)
+	} else {
+		if err != nil {
+			// If this does happen ever, we still want the user to get the fallback
+			// message and URL. So, we don't return with this error, but we do still
+			// want to print it.
+			fmt.Fprintf(opts.IO.ErrOut, "%v\n", err)
+		}
+		fmt.Fprintf(opts.IO.Out, "job %s queued. View progress: %s\n", job.ID, capi.AgentsHomeURL)
+	}
+
+	return nil
+}
+
+func agentSessionWebURL(repo ghrepo.Interface, j *capi.Job) string {
+	if j.PullRequest == nil {
+		return ""
+	}
+	if j.SessionID == "" {
+		return fmt.Sprintf("https://github.com/%s/%s/pull/%d", url.PathEscape(repo.RepoOwner()), url.PathEscape(repo.RepoName()), j.PullRequest.Number)
+	}
+	return fmt.Sprintf("https://github.com/%s/%s/pull/%d/agent-sessions/%s", url.PathEscape(repo.RepoOwner()), url.PathEscape(repo.RepoName()), j.PullRequest.Number, url.PathEscape(j.SessionID))
+}
+
+// fetchJobSessionURL tries to return the agent session URL for a job. If the pull
+// request is not yet available, ("", nil) is returned.
+func fetchJobSessionURL(ctx context.Context, client capi.CapiClient, repo ghrepo.Interface, job *capi.Job, bo backoff.BackOff) (string, error) {
+	if job.PullRequest != nil && job.PullRequest.Number > 0 {
+		// Return the agent session URL if we happen to get it.
+		// Right now, this never happens.
+		return agentSessionWebURL(repo, job), nil
+	}
+
+	if bo == nil {
+		bo = backoff.NewExponentialBackOff(
+			backoff.WithMaxElapsedTime(10*time.Second),
+			backoff.WithInitialInterval(300*time.Millisecond),
+			backoff.WithMaxInterval(10*time.Second),
+			backoff.WithMultiplier(1.5),
+		)
+	}
+
+	jobWithPR, err := fetchJobWithBackoff(ctx, client, repo, job.ID, bo)
+	if jobWithPR != nil {
+		return agentSessionWebURL(repo, jobWithPR), nil
+	}
+	return "", err
+}
+
+// fetchJobWithBackoff polls the job resource until a PR number is present or the overall
+// timeout elapses. It returns the updated Job on success, (nil, nil) on timeout,
+// and (nil, error) only for non-retryable failures.
+func fetchJobWithBackoff(ctx context.Context, client capi.CapiClient, repo ghrepo.Interface, jobID string, bo backoff.BackOff) (*capi.Job, error) {
+	// sentinel error to signal timeout
+	var errPRNotReady = errors.New("job not ready")
+
+	var result *capi.Job
+	retryErr := backoff.Retry(func() error {
+		j, err := client.GetJob(ctx, repo.RepoOwner(), repo.RepoName(), jobID)
+		if err != nil {
+			// Do not retry on GetJob errors; surface immediately.
+			return backoff.Permanent(err)
+		}
+		if j.PullRequest != nil && j.PullRequest.Number > 0 {
+			result = j
+			return nil
+		}
+		return errPRNotReady
+	}, backoff.WithContext(bo, ctx))
+
+	if retryErr != nil {
+		if errors.Is(retryErr, errPRNotReady) {
+			// Timed out
+			return nil, nil
+		}
+		return nil, retryErr
+	}
+	return result, nil
+}
+
+func followLogs(opts *CreateOptions, capiClient capi.CapiClient, sessionID string) error {
+	if err := opts.IO.StartPager(); err == nil {
+		defer opts.IO.StopPager()
+	} else {
+		fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
+	}
+
+	ctx := context.Background()
+	renderer := opts.LogRenderer()
+
+	var called bool
+	fetcher := func() ([]byte, error) {
+		if called {
+			opts.Sleep(defaultLogPollInterval)
+		}
+		called = true
+		raw, err := capiClient.GetSessionLogs(ctx, sessionID)
+		if err != nil {
+			return nil, err
+		}
+		return raw, nil
+	}
+
+	return renderer.Follow(fetcher, opts.IO.Out, opts.IO)
+}
diff --git a/pkg/cmd/agent-task/list/list.go b/pkg/cmd/agent-task/list/list.go
new file mode 100644
index 000000000..4370a0170
--- /dev/null
+++ b/pkg/cmd/agent-task/list/list.go
@@ -0,0 +1,150 @@
+package list
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/cli/cli/v2/internal/browser"
+	"github.com/cli/cli/v2/internal/tableprinter"
+	"github.com/cli/cli/v2/internal/text"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
+	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/spf13/cobra"
+)
+
+const defaultLimit = 30
+
+// ListOptions are the options for the list command
+type ListOptions struct {
+	IO         *iostreams.IOStreams
+	Limit      int
+	CapiClient func() (capi.CapiClient, error)
+	Web        bool
+	Browser    browser.Browser
+}
+
+// NewCmdList creates the list command
+func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
+	opts := &ListOptions{
+		IO:         f.IOStreams,
+		CapiClient: shared.CapiClientFunc(f),
+		Limit:      defaultLimit,
+		Browser:    f.Browser,
+	}
+
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: "List agent tasks (preview)",
+		Args:  cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if opts.Limit < 1 {
+				return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
+			}
+			if runF != nil {
+				return runF(opts)
+			}
+			return listRun(opts)
+		},
+	}
+
+	cmd.Flags().IntVarP(&opts.Limit, "limit", "L", defaultLimit, fmt.Sprintf("Maximum number of agent tasks to fetch (default %d)", defaultLimit))
+	cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open agent tasks in the browser")
+
+	return cmd
+}
+
+func listRun(opts *ListOptions) error {
+	if opts.Web {
+		webURL := capi.AgentsHomeURL
+		if opts.IO.IsStdoutTTY() {
+			fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(webURL))
+		}
+		return opts.Browser.Browse(webURL)
+	}
+
+	if opts.Limit <= 0 {
+		opts.Limit = defaultLimit
+	}
+
+	capiClient, err := opts.CapiClient()
+	if err != nil {
+		return err
+	}
+
+	opts.IO.StartProgressIndicatorWithLabel("Fetching agent tasks...")
+	defer opts.IO.StopProgressIndicator()
+	var sessions []*capi.Session
+	ctx := context.Background()
+
+	sessions, err = capiClient.ListLatestSessionsForViewer(ctx, opts.Limit)
+	if err != nil {
+		return err
+	}
+
+	opts.IO.StopProgressIndicator()
+
+	if len(sessions) == 0 {
+		return cmdutil.NewNoResultsError("no agent tasks found")
+	}
+
+	if err := opts.IO.StartPager(); err == nil {
+		defer opts.IO.StopPager()
+	} else {
+		fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
+	}
+
+	if opts.IO.IsStdoutTTY() {
+		count := len(sessions)
+		header := fmt.Sprintf("Showing %s", text.Pluralize(count, "session"))
+		fmt.Fprintf(opts.IO.Out, "%s\n\n", header)
+	}
+
+	cs := opts.IO.ColorScheme()
+	tp := tableprinter.New(opts.IO, tableprinter.WithHeader("Session Name", "Pull Request", "Repo", "Session State", "Created"))
+	for _, s := range sessions {
+		if s.ResourceType != "pull" || s.PullRequest == nil || s.PullRequest.Repository == nil {
+			// Skip these sessions in case they happen, for now.
+			continue
+		}
+
+		pr := fmt.Sprintf("#%d", s.PullRequest.Number)
+		repo := s.PullRequest.Repository.NameWithOwner
+
+		// Name
+		tp.AddField(s.Name)
+		if tp.IsTTY() {
+			tp.AddField(pr, tableprinter.WithColor(cs.ColorFromString(prShared.ColorForPRState(*s.PullRequest))))
+		} else {
+			tp.AddField(pr)
+		}
+
+		// Repo
+		tp.AddField(repo, tableprinter.WithColor(cs.Muted))
+
+		// State
+		if tp.IsTTY() {
+			tp.AddField(shared.SessionStateString(s.State), tableprinter.WithColor(shared.ColorFuncForSessionState(*s, cs)))
+		} else {
+			tp.AddField(shared.SessionStateString(s.State))
+		}
+
+		// Created
+		if tp.IsTTY() {
+			tp.AddTimeField(time.Now(), s.CreatedAt, cs.Muted)
+		} else {
+			tp.AddField(s.CreatedAt.Format(time.RFC3339))
+		}
+
+		tp.EndRow()
+	}
+
+	if err := tp.Render(); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/cmd/agent-task/shared/capi.go b/pkg/cmd/agent-task/shared/capi.go
new file mode 100644
index 000000000..c61aa13a7
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/capi.go
@@ -0,0 +1,54 @@
+package shared
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+)
+
+const uuidPattern = `[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}`
+
+var sessionIDRegexp = regexp.MustCompile(fmt.Sprintf("^%s$", uuidPattern))
+var agentSessionURLRegexp = regexp.MustCompile(fmt.Sprintf("^/agent-sessions/(%s)$", uuidPattern))
+
+func CapiClientFunc(f *cmdutil.Factory) func() (capi.CapiClient, error) {
+	return func() (capi.CapiClient, error) {
+		cfg, err := f.Config()
+		if err != nil {
+			return nil, err
+		}
+
+		httpClient, err := f.HttpClient()
+		if err != nil {
+			return nil, err
+		}
+
+		authCfg := cfg.Authentication()
+		return capi.NewCAPIClient(httpClient, authCfg), nil
+	}
+}
+
+func IsSessionID(s string) bool {
+	return sessionIDRegexp.MatchString(s)
+}
+
+// ParseSessionIDFromURL parses session ID from a pull request's agent session
+// URL, which is of the form:
+//
+//	`https://github.com/OWNER/REPO/pull/NUMBER/agent-sessions/SESSION-ID`
+func ParseSessionIDFromURL(u string) (string, error) {
+	_, _, rest, err := prShared.ParseURL(u)
+	if err != nil {
+		return "", err
+	}
+
+	match := agentSessionURLRegexp.FindStringSubmatch(rest)
+	if match == nil {
+		return "", errors.New("not a valid agent session URL")
+	}
+	return match[1], nil
+}
diff --git a/pkg/cmd/agent-task/shared/display.go b/pkg/cmd/agent-task/shared/display.go
new file mode 100644
index 000000000..3859a0e75
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/display.go
@@ -0,0 +1,63 @@
+package shared
+
+import (
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	"github.com/cli/cli/v2/pkg/iostreams"
+)
+
+// ColorFuncForSessionState returns a function that colors the session state
+func ColorFuncForSessionState(s capi.Session, cs *iostreams.ColorScheme) func(string) string {
+	var stateColor func(string) string
+	switch s.State {
+	case "completed":
+		stateColor = cs.Green
+	case "cancelled":
+		stateColor = cs.Muted
+	case "in_progress", "queued":
+		stateColor = cs.Yellow
+	case "failed":
+		stateColor = cs.Red
+	default:
+		stateColor = cs.Muted
+	}
+
+	return stateColor
+}
+
+// SessionStateString returns the humane/capitalised form of the given session state.
+func SessionStateString(state string) string {
+	switch state {
+	case "queued":
+		return "Queued"
+	case "in_progress":
+		return "In progress"
+	case "completed":
+		return "Ready for review"
+	case "failed":
+		return "Failed"
+	case "idle":
+		return "Idle"
+	case "waiting_for_user":
+		return "Waiting for user"
+	case "timed_out":
+		return "Timed out"
+	case "cancelled":
+		return "Cancelled"
+	default:
+		return state
+	}
+}
+
+type ColorFunc func(string) string
+
+func SessionSymbol(cs *iostreams.ColorScheme, state string) string {
+	noColor := func(s string) string { return s }
+	switch state {
+	case "completed":
+		return cs.SuccessIconWithColor(noColor)
+	case "failed", "timed_out", "cancelled":
+		return cs.FailureIconWithColor(noColor)
+	default:
+		return "-"
+	}
+}
diff --git a/pkg/cmd/agent-task/shared/log.go b/pkg/cmd/agent-task/shared/log.go
new file mode 100644
index 000000000..c94f5e603
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/log.go
@@ -0,0 +1,577 @@
+package shared
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"path/filepath"
+	"slices"
+	"strings"
+
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/cli/cli/v2/pkg/markdown"
+)
+
+//go:generate moq -rm -out log_mock.go . LogRenderer
+
+type LogRenderer interface {
+	Follow(fetcher func() ([]byte, error), w io.Writer, io *iostreams.IOStreams) error
+	Render(logs []byte, w io.Writer, io *iostreams.IOStreams) (stop bool, err error)
+}
+
+type logRenderer struct{}
+
+func NewLogRenderer() LogRenderer {
+	return &logRenderer{}
+}
+
+// Follow continuously fetches logs using the provided fetcher function and
+// renders them to the provided writer. It stops when Render indicates to stop.
+func (r *logRenderer) Follow(fetcher func() ([]byte, error), w io.Writer, io *iostreams.IOStreams) error {
+	var last string
+	for {
+		raw, err := fetcher()
+		if err != nil {
+			return err
+		}
+
+		logs := string(raw)
+		if logs == last {
+			continue
+		}
+
+		diff := strings.TrimSpace(logs[len(last):])
+
+		if stop, err := r.Render([]byte(diff), w, io); err != nil {
+			return err
+		} else if stop {
+			return nil
+		}
+
+		last = logs
+	}
+}
+
+// Render processes the given logs and writes the rendered output to w.
+// Errors are returned when an unexpected log entry is encountered.
+func (r *logRenderer) Render(logs []byte, w io.Writer, io *iostreams.IOStreams) (bool, error) {
+	lines := slices.DeleteFunc(strings.Split(string(logs), "\n"), func(line string) bool {
+		return line == ""
+	})
+
+	for _, line := range lines {
+		raw, found := strings.CutPrefix(line, "data: ")
+		if !found {
+			return false, errors.New("unexpected log format")
+		}
+
+		// The only log entry type we're interested in is a chat completion chunk,
+		// which can be verified by a successful unmarshal into the corresponding
+		// type AND the Object field being equal to "chat.completion.chunk". The
+		// latter is to avoid accepting an empty JSON object (i.e. "{}"). Also,
+		// if the entry is not what we expect, we should just skip and avoid
+		// returning an error.
+		var entry chatCompletionChunkEntry
+		err := json.Unmarshal([]byte(raw), &entry)
+		if err != nil || entry.Object != "chat.completion.chunk" {
+			continue
+		}
+
+		if stop, err := renderLogEntry(entry, w, io); err != nil {
+			return false, fmt.Errorf("failed to process log entry: %w", err)
+		} else if stop {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
+func renderLogEntry(entry chatCompletionChunkEntry, w io.Writer, io *iostreams.IOStreams) (bool, error) {
+	cs := io.ColorScheme()
+	var stop bool
+	for _, choice := range entry.Choices {
+		if choice.FinishReason == "stop" {
+			stop = true
+		}
+
+		if len(choice.Delta.ToolCalls) == 0 {
+			if choice.Delta.Content != "" && choice.Delta.Role == "assistant" {
+				// Copilot message and we should display.
+				renderRawMarkdown(choice.Delta.Content, w, io)
+			}
+			continue
+		}
+
+		// Since we don't want to clear-and-reprint live progress of events, we
+		// need to only process entries that correspond to a finished tool call.
+		// Such entries have a non-empty Content field.
+		if choice.Delta.Content == "" {
+			continue
+		}
+
+		if choice.Delta.ReasoningText != "" {
+			// Note that this should be formatted as a normal "thought" message,
+			// without the heading.
+			renderRawMarkdown(choice.Delta.ReasoningText, w, io)
+		}
+
+		for _, tc := range choice.Delta.ToolCalls {
+			name := tc.Function.Name
+			if name == "" {
+				continue
+			}
+
+			args := tc.Function.Arguments
+
+			switch name {
+			case "run_setup":
+				if v := unmarshal[runSetupToolArgs](args); v != nil {
+					renderToolCallTitle(w, cs, v.Name, "")
+					continue
+				}
+			case "view":
+				args := viewToolArgs{}
+				if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to parse 'view' tool call arguments: %v\n", err)
+					continue
+				}
+				renderToolCallTitle(w, cs, fmt.Sprintf("View %s", cs.Bold(relativeFilePath(args.Path))), "")
+
+				content := stripDiffFormat(choice.Delta.Content)
+
+				if err := renderFileContentAsMarkdown(args.Path, content, w, io); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to render viewed file content: %v\n\n", err)
+					fmt.Fprintln(io.ErrOut, content) // raw fallback
+				}
+			case "bash":
+				if v := unmarshal[bashToolArgs](args); v != nil {
+					if v.Description != "" {
+						renderToolCallTitle(w, cs, "Bash", v.Description)
+					} else {
+						renderToolCallTitle(w, cs, "Run Bash command", "")
+					}
+
+					contentWithCommand := choice.Delta.Content
+					if v.Command != "" {
+						contentWithCommand = fmt.Sprintf("$ %s\n%s", v.Command, choice.Delta.Content)
+					}
+					if err := renderFileContentAsMarkdown("commands.sh", contentWithCommand, w, io); err != nil {
+						fmt.Fprintf(io.ErrOut, "\nfailed to render bash command output: %v\n\n", err)
+						fmt.Fprintln(io.ErrOut, contentWithCommand)
+					}
+				}
+			// TODO: consider including more details for these bash-related tool calls.
+			case "write_bash":
+				if v := unmarshal[writeBashToolArgs](args); v != nil {
+					renderToolCallTitle(w, cs, "Send input to Bash session", "")
+					continue
+				}
+			case "read_bash":
+				if v := unmarshal[readBashToolArgs](args); v != nil {
+					renderToolCallTitle(w, cs, "Read logs from Bash session", "")
+					continue
+				}
+			case "stop_bash":
+				if v := unmarshal[stopBashToolArgs](args); v != nil {
+					renderToolCallTitle(w, cs, "Stop Bash session", "")
+					continue
+				}
+			case "async_bash":
+				if v := unmarshal[asyncBashToolArgs](args); v != nil {
+					renderToolCallTitle(w, cs, "Start or send input to long-running Bash session", "")
+					continue
+				}
+			case "read_async_bash":
+				if v := unmarshal[readAsyncBashToolArgs](args); v != nil {
+					renderToolCallTitle(w, cs, "View logs from long-running Bash session", "")
+					continue
+				}
+			case "stop_async_bash":
+				if v := unmarshal[stopAsyncBashToolArgs](args); v != nil {
+					renderToolCallTitle(w, cs, "Stop long-running Bash session", "")
+					continue
+				}
+			case "think":
+				args := thinkToolArgs{}
+				if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to parse 'think' tool call arguments: %v\n", err)
+					continue
+				}
+
+				// NOTE: omit the delta.content since it's the same as thought
+				renderToolCallTitle(w, cs, "Thought", "")
+				if err := renderRawMarkdown(args.Thought, w, io); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to render thought: %v\n", err)
+				}
+			case "report_progress":
+				args := reportProgressToolArgs{}
+				if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to parse 'report_progress' tool call arguments: %v\n", err)
+					continue
+				}
+
+				renderToolCallTitle(w, cs, "Progress update", cs.Bold(args.CommitMessage))
+				if args.PrDescription != "" {
+					if err := renderRawMarkdown(args.PrDescription, w, io); err != nil {
+						fmt.Fprintf(io.ErrOut, "\nfailed to render PR description: %v\n", err)
+					}
+				}
+
+				// TODO: KW I wasn't able to get this case to populate ever.
+				if choice.Delta.Content != "" {
+					// Try to treat this as JSON
+					if err := renderContentAsJSONMarkdown("", choice.Delta.Content, w, io); err != nil {
+						fmt.Fprintf(io.ErrOut, "\nfailed to render progress update content: %v\n", err)
+					}
+				}
+
+			case "create":
+				args := createToolArgs{}
+				if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to parse 'create' tool call arguments: %v\n", err)
+					continue
+				}
+				renderToolCallTitle(w, cs, "Create", cs.Bold(relativeFilePath(args.Path)))
+
+				if err := renderFileContentAsMarkdown(args.Path, args.FileText, w, io); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to render created file content: %v\n\n", err)
+					fmt.Fprintln(io.ErrOut, args.FileText)
+				}
+			case "str_replace":
+				args := strReplaceToolArgs{}
+				if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to parse 'str_replace' tool call arguments: %v\n", err)
+					continue
+				}
+
+				renderToolCallTitle(w, cs, "Edit", cs.Bold(relativeFilePath(args.Path)))
+				if err := renderFileContentAsMarkdown("output.diff", choice.Delta.Content, w, io); err != nil {
+					fmt.Fprintf(io.ErrOut, "\nfailed to render str_replace diff: %v\n\n", err)
+					fmt.Fprintln(io.ErrOut, choice.Delta.Content)
+				}
+			default:
+				// Unknown tool call. For example for "codeql_checker":
+				// NOTE: omit the delta.content since we don't know how large could that be
+				renderGenericToolCall(w, cs, name)
+
+				// If it's JSON, treat it as such, otherwise we skip whatever the content is.
+				_ = renderContentAsJSONMarkdown("Output:", choice.Delta.Content, w, io)
+
+				// The entirety of the args can be treated as "input" to the tool call.
+				// We try to render it as JSON, but if that fails, just skip it.
+				_ = renderContentAsJSONMarkdown("Input:", args, w, io)
+			}
+		}
+	}
+	return stop, nil
+}
+
+// renderContentAsJSONMarkdown tries to unmarshal the given content as JSON,
+// wrap that content in a markdown JSON code block, and render it as markdown.
+// If label is non-empty, it is rendered as leading text before and outside of
+// the JSON block.
+func renderContentAsJSONMarkdown(label, content string, w io.Writer, io *iostreams.IOStreams) error {
+	var contentAsJSON any
+	if err := json.Unmarshal([]byte(content), &contentAsJSON); err == nil {
+		marshaled, err := json.MarshalIndent(contentAsJSON, "", "  ")
+		if err != nil {
+			return fmt.Errorf("failed to marshal JSON: %w", err)
+		}
+
+		if label != "" {
+			if err := renderRawMarkdown(label, w, io); err != nil {
+				return fmt.Errorf("failed to render label: %w", err)
+			}
+		}
+
+		if err := renderFileContentAsMarkdown("output.json", string(marshaled), w, io); err != nil {
+			return fmt.Errorf("failed to render JSON: %w", err)
+		}
+	}
+	return nil
+}
+
+// renderRawMarkdown renders the given raw markdown string to the given writer.
+// Use for complete markdown content from tool calls that need no conversion.
+func renderRawMarkdown(md string, w io.Writer, io *iostreams.IOStreams) error {
+	// Glamour doesn't add leading newlines when content is a complete
+	// markdown document. So, we must add the leading newline.
+	formatFunc := func(s string) string {
+		return fmt.Sprintf("\n%s\n\n", s)
+	}
+
+	return renderMarkdownWithFormat(md, w, io, formatFunc)
+}
+
+// renderMarkdownWithFormat renders the given markdown string to the given writer.
+// If a formatFunc is provided, the md string is ran through it before
+// rendering. This can be used to add newlines before and after the content.
+func renderMarkdownWithFormat(md string, w io.Writer, io *iostreams.IOStreams, formatFunc func(string) string) error {
+	rendered, err := markdown.Render(md,
+		markdown.WithTheme(io.TerminalTheme()),
+		markdown.WithWrap(io.TerminalWidth()),
+	)
+
+	if err != nil {
+		return fmt.Errorf("failed to render markdown: %w", err)
+	}
+
+	rendered = strings.TrimSpace(rendered)
+	if formatFunc != nil {
+		rendered = formatFunc(rendered)
+	}
+
+	fmt.Fprint(w, rendered)
+
+	return nil
+}
+
+// stripDiffFormat implements a primitive conversion from a diff string to a
+// plain text representation by removing diff-specific formatting.
+func stripDiffFormat(diff string) string {
+	lines := strings.Split(diff, "\n")
+
+	// Find where the hunk header ends.
+	hunkEndIndex := -1
+	for i, line := range lines {
+		if strings.HasPrefix(line, "@@") {
+			hunkEndIndex = i
+			break
+		}
+	}
+
+	// This isn't a diff.
+	if hunkEndIndex == -1 {
+		return diff
+	}
+
+	// Removing hunk header.
+	lines = lines[hunkEndIndex+1:]
+
+	// Strip the leading + and - from lines, if they exist.
+	var stripped []string
+	for _, line := range lines {
+		if strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") {
+			stripped = append(stripped, line[1:])
+		} else {
+			stripped = append(stripped, line)
+		}
+	}
+	return strings.Join(stripped, "\n")
+}
+
+// renderFileContentAsMarkdown renders the given content as markdown
+// based on the file extension of the path.
+func renderFileContentAsMarkdown(path, content string, w io.Writer, io *iostreams.IOStreams) error {
+	lang := filepath.Ext(filepath.ToSlash(path))
+
+	if lang == ".md" {
+		return renderRawMarkdown(content, w, io)
+	}
+
+	md := fmt.Sprintf("```%s\n%s\n```", lang, content)
+	// Glamour adds leading newlines when content is only a code block,
+	// so we only want to add a trailing newline.
+	formatFunc := func(s string) string {
+		return fmt.Sprintf("%s\n\n", s)
+	}
+
+	return renderMarkdownWithFormat(md, w, io, formatFunc)
+}
+
+// relativeFilePath converts an absolute file path to a relative one.
+// We expect paths to be of the form: /home/runner/work/<repo-owner>/<repo-name>/path/to/file
+// The expected output of that example is: path/to/file
+func relativeFilePath(absPath string) string {
+	relPath := strings.TrimPrefix(absPath, "/home/runner/work/")
+
+	parts := strings.Split(relPath, "/")
+
+	// The last two parts of the path are the
+	// repo name and the repo owner.
+	// If that's all we have (or less),
+	// we return a friendly name "repository".
+	if len(parts) > 2 {
+		// Drop the repo owner and name, returning the remaining path.
+		return strings.Join(parts[2:], "/")
+	}
+	return "repository"
+}
+
+func unmarshal[T any](raw string) *T {
+	var t T
+	if err := json.Unmarshal([]byte(raw), &t); err != nil {
+		return nil
+	}
+	return &t
+}
+
+// renderToolCallTitle renders a title for a tool call. Should be followed by a
+// call to render a markdown representation of the tool call's content.
+func renderToolCallTitle(w io.Writer, cs *iostreams.ColorScheme, toolName, title string) {
+	// Should not happen, but if it does we still want to print a heading
+	// with the information we do have.
+	if toolName == "" {
+		toolName = "Generic tool call"
+	}
+
+	if title != "" {
+		title = cs.Bold(title)
+	}
+
+	if title != "" {
+		fmt.Fprintf(w, "%s: %s\n", toolName, title)
+	} else {
+		fmt.Fprintf(w, "%s\n", toolName)
+	}
+}
+
+// genericToolCallNamesToTitles maps known generic tool call identifiers to human-friendly titles.
+var genericToolCallNamesToTitles = map[string]string{
+	// Custom tools, the GitHub UI doesn't currently have these.
+	"codeql_checker": "Run CodeQL analysis",
+
+	// Playwright tools.
+	"playwright-browser_navigate":         "Navigate Playwright web browser to a URL",
+	"playwright-browser_navigate_back":    "Navigate back in Playwright web browser",
+	"playwright-browser_navigate_forward": "Navigate forward in Playwright web browser",
+	"playwright-browser_click":            "Click element in Playwright web browser",
+	"playwright-browser_take_screenshot":  "Take screenshot of Playwright web browser",
+	"playwright-browser_type":             "Type in Playwright web browser",
+	"playwright-browser_wait_for":         "Wait for text to appear/disappear in Playwright web browser",
+	"playwright-browser_evaluate":         "Run JavaScript in Playwright web browser",
+	"playwright-browser_snapshot":         "Take snapshot of page in Playwright web browser",
+	"playwright-browser_resize":           "Resize Playwright web browser window",
+	"playwright-browser_close":            "Close Playwright web browser",
+	"playwright-browser_press_key":        "Press key in Playwright web browser",
+	"playwright-browser_select_option":    "Select option in Playwright web browser",
+	"playwright-browser_handle_dialog":    "Interact with dialog in Playwright web browser",
+	"playwright-browser_console_messages": "Get console messages from Playwright web browser",
+	"playwright-browser_drag":             "Drag mouse between elements in Playwright web browser",
+	"playwright-browser_file_upload":      "Upload file in Playwright web browser",
+	"playwright-browser_hover":            "Hover mouse over element in Playwright web browser",
+	"playwright-browser_network_requests": "Get network requests from Playwright web browser",
+
+	// GitHub MCP server common tools
+	"github-mcp-server-get_file_contents":              "Get file contents from GitHub",
+	"github-mcp-server-get_pull_request":               "Get pull request from GitHub",
+	"github-mcp-server-get_issue":                      "Get issue from GitHub",
+	"github-mcp-server-get_pull_request_files":         "Get pull request changed files from GitHub",
+	"github-mcp-server-list_pull_requests":             "List pull requests on GitHub",
+	"github-mcp-server-list_branches":                  "List branches on GitHub",
+	"github-mcp-server-get_pull_request_diff":          "Get pull request diff from GitHub",
+	"github-mcp-server-get_pull_request_comments":      "Get pull request comments from GitHub",
+	"github-mcp-server-get_commit":                     "Get commit from GitHub",
+	"github-mcp-server-search_repositories":            "Search repositories on GitHub",
+	"github-mcp-server-search_code":                    "Search code on GitHub",
+	"github-mcp-server-get_issue_comments":             "Get issue comments from GitHub",
+	"github-mcp-server-list_issues":                    "List issues on GitHub",
+	"github-mcp-server-search_pull_requests":           "Search pull requests on GitHub",
+	"github-mcp-server-list_commits":                   "List commits on GitHub",
+	"github-mcp-server-get_pull_request_status":        "Get pull request status from GitHub",
+	"github-mcp-server-search_issues":                  "Search issues on GitHub",
+	"github-mcp-server-get_pull_request_reviews":       "Get pull request reviews from GitHub",
+	"github-mcp-server-download_workflow_run_artifact": "Download GitHub Actions workflow run artifact",
+	"github-mcp-server-get_job_logs":                   "Get GitHub Actions job logs",
+	"github-mcp-server-get_workflow_run":               "Get GitHub Actions workflow run",
+	"github-mcp-server-get_workflow_run_logs":          "Get GitHub Actions workflow run logs",
+	"github-mcp-server-get_workflow_run_usage":         "Get GitHub Actions workflow usage",
+	"github-mcp-server-list_workflow_jobs":             "List GitHub Actions workflow jobs",
+	"github-mcp-server-list_workflow_run_artifacts":    "List GitHub Actions workflow run artifacts",
+	"github-mcp-server-list_workflow_runs":             "List GitHub Actions workflow runs",
+	"github-mcp-server-list_workflows":                 "List GitHub Actions workflows",
+}
+
+func renderGenericToolCall(w io.Writer, cs *iostreams.ColorScheme, name string) {
+	toolName, ok := genericToolCallNamesToTitles[name]
+	if !ok {
+		toolName = fmt.Sprintf("Call to %s", name)
+	}
+
+	renderToolCallTitle(w, cs, toolName, "")
+}
+
+type chatCompletionChunkEntry struct {
+	ID      string `json:"id"`
+	Created int64  `json:"created"`
+	Model   string `json:"model"`
+	Object  string `json:"object"`
+	Choices []struct {
+		Delta struct {
+			ReasoningText string `json:"reasoning_text"`
+			Content       string `json:"content"`
+			Role          string `json:"role"`
+			ToolCalls     []struct {
+				Function struct {
+					Name      string `json:"name"`
+					Arguments string `json:"arguments"`
+				} `json:"function"`
+				Index int    `json:"index"`
+				ID    string `json:"id"`
+			} `json:"tool_calls"`
+		} `json:"delta"`
+		FinishReason string `json:"finish_reason"`
+		Index        int    `json:"index"`
+	} `json:"choices"`
+}
+
+type runSetupToolArgs struct {
+	Name string `json:"name"`
+}
+
+type bashToolArgs struct {
+	Command     string `json:"command"`
+	Description string `json:"description"`
+}
+
+type readBashToolArgs struct {
+	SessionID string `json:"sessionId"`
+}
+
+type writeBashToolArgs struct {
+	SessionID string `json:"sessionId"`
+	Input     string `json:"input"`
+}
+
+type stopBashToolArgs struct {
+	SessionID string `json:"sessionId"`
+}
+
+type asyncBashToolArgs struct {
+	Command   string `json:"command"`
+	SessionID string `json:"sessionId"`
+}
+
+type readAsyncBashToolArgs struct {
+	SessionID string `json:"sessionId"`
+}
+
+type stopAsyncBashToolArgs struct {
+	SessionID string `json:"sessionId"`
+}
+
+type viewToolArgs struct {
+	Path string `json:"path"`
+}
+type thinkToolArgs struct {
+	SessionID string `json:"sessionId"`
+	Thought   string `json:"thought"`
+}
+
+type reportProgressToolArgs struct {
+	CommitMessage string `json:"commitMessage"`
+	PrDescription string `json:"prDescription"`
+}
+
+type createToolArgs struct {
+	FileText string `json:"file_text"`
+	Path     string `json:"path"`
+}
+
+type strReplaceToolArgs struct {
+	NewStr string `json:"new_str"`
+	OldStr string `json:"old_str"`
+	Path   string `json:"path"`
+}
diff --git a/pkg/cmd/agent-task/shared/log_mock.go b/pkg/cmd/agent-task/shared/log_mock.go
new file mode 100644
index 000000000..f12cf6bf3
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/log_mock.go
@@ -0,0 +1,144 @@
+// Code generated by moq; DO NOT EDIT.
+// github.com/matryer/moq
+
+package shared
+
+import (
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"io"
+	"sync"
+)
+
+// Ensure, that LogRendererMock does implement LogRenderer.
+// If this is not the case, regenerate this file with moq.
+var _ LogRenderer = &LogRendererMock{}
+
+// LogRendererMock is a mock implementation of LogRenderer.
+//
+//	func TestSomethingThatUsesLogRenderer(t *testing.T) {
+//
+//		// make and configure a mocked LogRenderer
+//		mockedLogRenderer := &LogRendererMock{
+//			FollowFunc: func(fetcher func() ([]byte, error), w io.Writer, ioMoqParam *iostreams.IOStreams) error {
+//				panic("mock out the Follow method")
+//			},
+//			RenderFunc: func(logs []byte, w io.Writer, ioMoqParam *iostreams.IOStreams) (bool, error) {
+//				panic("mock out the Render method")
+//			},
+//		}
+//
+//		// use mockedLogRenderer in code that requires LogRenderer
+//		// and then make assertions.
+//
+//	}
+type LogRendererMock struct {
+	// FollowFunc mocks the Follow method.
+	FollowFunc func(fetcher func() ([]byte, error), w io.Writer, ioMoqParam *iostreams.IOStreams) error
+
+	// RenderFunc mocks the Render method.
+	RenderFunc func(logs []byte, w io.Writer, ioMoqParam *iostreams.IOStreams) (bool, error)
+
+	// calls tracks calls to the methods.
+	calls struct {
+		// Follow holds details about calls to the Follow method.
+		Follow []struct {
+			// Fetcher is the fetcher argument value.
+			Fetcher func() ([]byte, error)
+			// W is the w argument value.
+			W io.Writer
+			// IoMoqParam is the ioMoqParam argument value.
+			IoMoqParam *iostreams.IOStreams
+		}
+		// Render holds details about calls to the Render method.
+		Render []struct {
+			// Logs is the logs argument value.
+			Logs []byte
+			// W is the w argument value.
+			W io.Writer
+			// IoMoqParam is the ioMoqParam argument value.
+			IoMoqParam *iostreams.IOStreams
+		}
+	}
+	lockFollow sync.RWMutex
+	lockRender sync.RWMutex
+}
+
+// Follow calls FollowFunc.
+func (mock *LogRendererMock) Follow(fetcher func() ([]byte, error), w io.Writer, ioMoqParam *iostreams.IOStreams) error {
+	if mock.FollowFunc == nil {
+		panic("LogRendererMock.FollowFunc: method is nil but LogRenderer.Follow was just called")
+	}
+	callInfo := struct {
+		Fetcher    func() ([]byte, error)
+		W          io.Writer
+		IoMoqParam *iostreams.IOStreams
+	}{
+		Fetcher:    fetcher,
+		W:          w,
+		IoMoqParam: ioMoqParam,
+	}
+	mock.lockFollow.Lock()
+	mock.calls.Follow = append(mock.calls.Follow, callInfo)
+	mock.lockFollow.Unlock()
+	return mock.FollowFunc(fetcher, w, ioMoqParam)
+}
+
+// FollowCalls gets all the calls that were made to Follow.
+// Check the length with:
+//
+//	len(mockedLogRenderer.FollowCalls())
+func (mock *LogRendererMock) FollowCalls() []struct {
+	Fetcher    func() ([]byte, error)
+	W          io.Writer
+	IoMoqParam *iostreams.IOStreams
+} {
+	var calls []struct {
+		Fetcher    func() ([]byte, error)
+		W          io.Writer
+		IoMoqParam *iostreams.IOStreams
+	}
+	mock.lockFollow.RLock()
+	calls = mock.calls.Follow
+	mock.lockFollow.RUnlock()
+	return calls
+}
+
+// Render calls RenderFunc.
+func (mock *LogRendererMock) Render(logs []byte, w io.Writer, ioMoqParam *iostreams.IOStreams) (bool, error) {
+	if mock.RenderFunc == nil {
+		panic("LogRendererMock.RenderFunc: method is nil but LogRenderer.Render was just called")
+	}
+	callInfo := struct {
+		Logs       []byte
+		W          io.Writer
+		IoMoqParam *iostreams.IOStreams
+	}{
+		Logs:       logs,
+		W:          w,
+		IoMoqParam: ioMoqParam,
+	}
+	mock.lockRender.Lock()
+	mock.calls.Render = append(mock.calls.Render, callInfo)
+	mock.lockRender.Unlock()
+	return mock.RenderFunc(logs, w, ioMoqParam)
+}
+
+// RenderCalls gets all the calls that were made to Render.
+// Check the length with:
+//
+//	len(mockedLogRenderer.RenderCalls())
+func (mock *LogRendererMock) RenderCalls() []struct {
+	Logs       []byte
+	W          io.Writer
+	IoMoqParam *iostreams.IOStreams
+} {
+	var calls []struct {
+		Logs       []byte
+		W          io.Writer
+		IoMoqParam *iostreams.IOStreams
+	}
+	mock.lockRender.RLock()
+	calls = mock.calls.Render
+	mock.lockRender.RUnlock()
+	return calls
+}
diff --git a/pkg/cmd/agent-task/view/view.go b/pkg/cmd/agent-task/view/view.go
new file mode 100644
index 000000000..38c1e0e12
--- /dev/null
+++ b/pkg/cmd/agent-task/view/view.go
@@ -0,0 +1,398 @@
+package view
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/internal/browser"
+	"github.com/cli/cli/v2/internal/ghinstance"
+	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
+	"github.com/cli/cli/v2/internal/text"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
+	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/spf13/cobra"
+)
+
+const (
+	defaultLimit           = 40
+	defaultLogPollInterval = 5 * time.Second
+)
+
+type ViewOptions struct {
+	IO         *iostreams.IOStreams
+	BaseRepo   func() (ghrepo.Interface, error)
+	CapiClient func() (capi.CapiClient, error)
+	HttpClient func() (*http.Client, error)
+	Finder     prShared.PRFinder
+	Prompter   prompter.Prompter
+	Browser    browser.Browser
+
+	LogRenderer func() shared.LogRenderer
+	Sleep       func(d time.Duration)
+
+	SelectorArg string
+	PRNumber    int
+	SessionID   string
+	Web         bool
+	Log         bool
+	Follow      bool
+}
+
+func defaultLogRenderer() shared.LogRenderer {
+	return shared.NewLogRenderer()
+}
+
+func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
+	opts := &ViewOptions{
+		IO:          f.IOStreams,
+		HttpClient:  f.HttpClient,
+		CapiClient:  shared.CapiClientFunc(f),
+		Prompter:    f.Prompter,
+		Browser:     f.Browser,
+		LogRenderer: defaultLogRenderer,
+		Sleep:       time.Sleep,
+	}
+
+	cmd := &cobra.Command{
+		Use:   "view [<session-id> | <pr-number> | <pr-url> | <pr-branch>]",
+		Short: "View an agent task session (preview)",
+		Long: heredoc.Doc(`
+			View an agent task session.
+		`),
+		Example: heredoc.Doc(`
+			# View an agent task by session ID
+			$ gh agent-task view e2fa49d2-f164-4a56-ab99-498090b8fcdf
+
+			# View an agent task by pull request number in current repo
+			$ gh agent-task view 12345
+
+			# View an agent task by pull request number
+			$ gh agent-task view --repo OWNER/REPO 12345
+
+			# View an agent task by pull request reference
+			$ gh agent-task view OWNER/REPO#12345
+
+			# View a pull request agents tasks in the browser
+			$ gh agent-task view 12345 --web
+		`),
+		Args: cobra.MaximumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			// Support -R/--repo override
+			opts.BaseRepo = f.BaseRepo
+
+			if len(args) > 0 {
+				opts.SelectorArg = args[0]
+				if shared.IsSessionID(opts.SelectorArg) {
+					opts.SessionID = opts.SelectorArg
+				} else if sessionID, err := shared.ParseSessionIDFromURL(opts.SelectorArg); err == nil {
+					opts.SessionID = sessionID
+				}
+			}
+
+			if opts.SessionID == "" && !opts.IO.CanPrompt() {
+				return fmt.Errorf("session ID is required when not running interactively")
+			}
+
+			if opts.Follow && !opts.Log {
+				return cmdutil.FlagErrorf("--log is required when providing --follow")
+			}
+
+			if opts.Finder == nil {
+				opts.Finder = prShared.NewFinder(f)
+			}
+
+			if runF != nil {
+				return runF(opts)
+			}
+			return viewRun(opts)
+		},
+	}
+
+	cmdutil.EnableRepoOverride(cmd, f)
+
+	cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open agent task in the browser")
+	cmd.Flags().BoolVar(&opts.Log, "log", false, "Show agent session logs")
+	cmd.Flags().BoolVar(&opts.Follow, "follow", false, "Follow agent session logs")
+
+	return cmd
+}
+
+func viewRun(opts *ViewOptions) error {
+	capiClient, err := opts.CapiClient()
+	if err != nil {
+		return err
+	}
+
+	ctx := context.Background()
+	cs := opts.IO.ColorScheme()
+
+	opts.IO.StartProgressIndicatorWithLabel("Fetching agent session...")
+	defer opts.IO.StopProgressIndicator()
+
+	var session *capi.Session
+
+	if opts.SessionID != "" {
+		sess, err := capiClient.GetSession(ctx, opts.SessionID)
+		if err != nil {
+			if errors.Is(err, capi.ErrSessionNotFound) {
+				fmt.Fprintln(opts.IO.ErrOut, "session not found")
+				return cmdutil.SilentError
+			}
+			return err
+		}
+
+		opts.IO.StopProgressIndicator()
+
+		if opts.Web {
+			var webURL string
+			if sess.PullRequest != nil {
+				webURL = fmt.Sprintf("%s/agent-sessions/%s", sess.PullRequest.URL, url.PathEscape(sess.ID))
+			} else {
+				// Currently the web Copilot Agents home GUI does not support focusing
+				// on a given session, so we should just navigate to the home page.
+				webURL = capi.AgentsHomeURL
+			}
+
+			if opts.IO.IsStdoutTTY() {
+				fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(webURL))
+			}
+			return opts.Browser.Browse(webURL)
+		}
+
+		session = sess
+	} else {
+		var prID int64
+		var prURL string
+
+		if opts.SelectorArg != "" {
+			// Finder does not support the PR/issue reference format (e.g. owner/repo#123)
+			// so we need to check if the selector arg is a reference and fetch the PR
+			// directly.
+			if repo, num, err := prShared.ParseFullReference(opts.SelectorArg); err == nil {
+				// Since the selector was a reference (i.e. without hostname data), we need to
+				// check the base repo to get the hostname.
+				baseRepo, err := opts.BaseRepo()
+				if err != nil {
+					return err
+				}
+
+				hostname := baseRepo.RepoHost()
+				if hostname != ghinstance.Default() {
+					return fmt.Errorf("agent tasks are not supported on this host: %s", hostname)
+				}
+
+				prID, prURL, err = capiClient.GetPullRequestDatabaseID(ctx, hostname, repo.RepoOwner(), repo.RepoName(), num)
+				if err != nil {
+					return fmt.Errorf("failed to fetch pull request: %w", err)
+				}
+			}
+		}
+
+		if prID == 0 {
+			findOptions := prShared.FindOptions{
+				Selector:        opts.SelectorArg,
+				Fields:          []string{"id", "url", "fullDatabaseId"},
+				DisableProgress: true,
+			}
+
+			pr, repo, err := opts.Finder.Find(findOptions)
+			if err != nil {
+				return err
+			}
+
+			if repo.RepoHost() != ghinstance.Default() {
+				return fmt.Errorf("agent tasks are not supported on this host: %s", repo.RepoHost())
+			}
+
+			databaseID, err := strconv.ParseInt(pr.FullDatabaseID, 10, 64)
+			if err != nil {
+				return fmt.Errorf("failed to parse pull request: %w", err)
+			}
+
+			prID = databaseID
+			prURL = pr.URL
+		}
+
+		sessions, err := capiClient.ListSessionsByResourceID(ctx, "pull", prID, defaultLimit)
+		if err != nil {
+			return fmt.Errorf("failed to list sessions for pull request: %w", err)
+		}
+
+		if len(sessions) == 0 {
+			fmt.Fprintln(opts.IO.ErrOut, "no session found for pull request")
+			return cmdutil.SilentError
+		}
+
+		opts.IO.StopProgressIndicator()
+
+		if opts.Web {
+			// Note that, we needed to make sure the PR exists and it has at least one session
+			// associated with it, other wise the `/agent-sessions` page would display the 404
+			// error.
+
+			// We don't need to navigate to a specific session; if there's only one session
+			// then the GUI will automatically show it, otherwise the user can select from the
+			// list. This is to avoid unnecessary prompting.
+			webURL := prURL + "/agent-sessions"
+			if opts.IO.IsStdoutTTY() {
+				fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(webURL))
+			}
+			return opts.Browser.Browse(webURL)
+		}
+
+		selectedSession := sessions[0]
+		if len(sessions) > 1 {
+			now := time.Now()
+			options := make([]string, 0, len(sessions))
+			for _, session := range sessions {
+				options = append(options, fmt.Sprintf(
+					"%s %s • updated %s",
+					shared.SessionSymbol(cs, session.State),
+					session.Name,
+					text.FuzzyAgo(now, session.LastUpdatedAt),
+				))
+			}
+
+			selected, err := opts.Prompter.Select("Select a session", "", options)
+			if err != nil {
+				return err
+			}
+
+			selectedSession = sessions[selected]
+		}
+
+		opts.IO.StartProgressIndicatorWithLabel("Fetching agent session...")
+		defer opts.IO.StopProgressIndicator()
+
+		// Sessions returned by ListSessionsByResourceID do not have all fields populated.
+		// So, we need to fetch the individual session to get all the details.
+		session, err = capiClient.GetSession(ctx, selectedSession.ID)
+		if err != nil {
+			return err
+		}
+
+		opts.IO.StopProgressIndicator()
+	}
+
+	if opts.Log {
+		return printLogs(opts, capiClient, session.ID)
+	}
+
+	printSession(opts, session)
+	return nil
+}
+
+func printSession(opts *ViewOptions, session *capi.Session) {
+	cs := opts.IO.ColorScheme()
+
+	fmt.Fprintf(opts.IO.Out, "%s • %s\n",
+		shared.ColorFuncForSessionState(*session, cs)(shared.SessionStateString(session.State)),
+		cs.Bold(session.Name),
+	)
+
+	if session.User != nil {
+		fmt.Fprintf(opts.IO.Out, "Started on behalf of %s %s\n", session.User.Login, text.FuzzyAgo(time.Now(), session.CreatedAt))
+	} else {
+		// Should never happen, but we need to cover the path
+		fmt.Fprintf(opts.IO.Out, "Started %s\n", text.FuzzyAgo(time.Now(), session.CreatedAt))
+	}
+
+	usedPremiumRequests := strings.TrimSuffix(fmt.Sprintf("%.1f", session.PremiumRequests), ".0")
+	usedPremiumRequestsNote := fmt.Sprintf("Used %s premium request(s)", usedPremiumRequests)
+
+	var durationNote string
+	if session.CompletedAt.After(session.CreatedAt) {
+		durationNote = fmt.Sprintf(" • Duration %s", session.CompletedAt.Sub(session.CreatedAt).Round(time.Second).String())
+	}
+
+	fmt.Fprintf(opts.IO.Out, "%s%s\n", cs.Muted(usedPremiumRequestsNote), cs.Muted(durationNote))
+
+	// Note that when the session is just created, a PR is not yet available for it.
+	if session.PullRequest != nil {
+		fmt.Fprintf(opts.IO.Out, "\n%s%s • %s\n",
+			session.PullRequest.Repository.NameWithOwner,
+			cs.ColorFromString(prShared.ColorForPRState(*session.PullRequest))(fmt.Sprintf("#%d", session.PullRequest.Number)),
+			cs.Bold(session.PullRequest.Title),
+		)
+	}
+
+	if session.Error != nil {
+		var workflowRunURL string
+		if session.WorkflowRunID != 0 && session.PullRequest != nil {
+			if u, err := url.Parse(session.PullRequest.URL); err == nil {
+				workflowRunURL = fmt.Sprintf("%s://%s/%s/actions/runs/%d", u.Scheme, u.Host, session.PullRequest.Repository.NameWithOwner, session.WorkflowRunID)
+			}
+		}
+
+		message := session.Error.Message
+		if message == "" {
+			message = "An error occurred"
+		}
+		fmt.Fprintf(opts.IO.Out, "\n%s %s\n", cs.FailureIconWithColor(cs.Red), message)
+
+		if workflowRunURL != "" {
+			// We don't need to prefix the link with any text (e.g. "checkout the logs here")
+			// because the error message already contains all the information.
+			fmt.Fprintf(opts.IO.Out, "%s\n", workflowRunURL)
+		}
+	}
+
+	if !opts.Log {
+		fmt.Fprint(opts.IO.Out, cs.Mutedf("\nFor detailed session logs, try:\ngh agent-task view '%s' --log\n", session.ID))
+	} else if !opts.Follow {
+		fmt.Fprint(opts.IO.Out, cs.Mutedf("\nTo follow session logs, try:\ngh agent-task view '%s' --log --follow\n", session.ID))
+	}
+
+	if session.PullRequest != nil {
+		fmt.Fprintln(opts.IO.Out, cs.Muted("\nView this session on GitHub:"))
+		fmt.Fprintln(opts.IO.Out, cs.Muted(fmt.Sprintf("%s/agent-sessions/%s", session.PullRequest.URL, url.PathEscape(session.ID))))
+	}
+}
+
+func printLogs(opts *ViewOptions, capiClient capi.CapiClient, sessionID string) error {
+	ctx := context.Background()
+
+	renderer := opts.LogRenderer()
+
+	if err := opts.IO.StartPager(); err == nil {
+		defer opts.IO.StopPager()
+	} else {
+		fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
+	}
+
+	if opts.Follow {
+		var called bool
+		fetcher := func() ([]byte, error) {
+			if called {
+				opts.Sleep(defaultLogPollInterval)
+			}
+			called = true
+			raw, err := capiClient.GetSessionLogs(ctx, sessionID)
+			if err != nil {
+				return nil, err
+			}
+			return raw, nil
+		}
+
+		return renderer.Follow(fetcher, opts.IO.Out, opts.IO)
+	}
+
+	raw, err := capiClient.GetSessionLogs(ctx, sessionID)
+	if err != nil {
+		return fmt.Errorf("failed to fetch session logs: %w", err)
+	}
+
+	_, err = renderer.Render(raw, opts.IO.Out, opts.IO)
+	return err
+}
diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go
index 73ee084e9..348b9531d 100644
--- a/pkg/cmd/auth/status/status.go
+++ b/pkg/cmd/auth/status/status.go
@@ -19,113 +19,106 @@ import (
 	"github.com/spf13/cobra"
 )
 
-type validEntry struct {
-	active      bool
-	host        string
-	user        string
-	token       string
-	tokenSource string
-	gitProtocol string
-	scopes      string
-}
-
-func (e validEntry) String(cs *iostreams.ColorScheme) string {
-	var sb strings.Builder
+type authEntryState string
 
-	sb.WriteString(
-		fmt.Sprintf("  %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.host, cs.Bold(e.user), e.tokenSource),
-	)
-	activeStr := fmt.Sprintf("%v", e.active)
-	sb.WriteString(fmt.Sprintf("  - Active account: %s\n", cs.Bold(activeStr)))
-	sb.WriteString(fmt.Sprintf("  - Git operations protocol: %s\n", cs.Bold(e.gitProtocol)))
-	sb.WriteString(fmt.Sprintf("  - Token: %s\n", cs.Bold(e.token)))
-
-	if expectScopes(e.token) {
-		sb.WriteString(fmt.Sprintf("  - Token scopes: %s\n", cs.Bold(displayScopes(e.scopes))))
-		if err := shared.HeaderHasMinimumScopes(e.scopes); err != nil {
-			var missingScopesError *shared.MissingScopesError
-			if errors.As(err, &missingScopesError) {
-				missingScopes := strings.Join(missingScopesError.MissingScopes, ",")
-				sb.WriteString(fmt.Sprintf("  %s Missing required token scopes: %s\n",
-					cs.WarningIcon(),
-					cs.Bold(displayScopes(missingScopes))))
-				refreshInstructions := fmt.Sprintf("gh auth refresh -h %s", e.host)
-				sb.WriteString(fmt.Sprintf("  - To request missing scopes, run: %s\n", cs.Bold(refreshInstructions)))
-			}
-		}
-	}
+const (
+	authEntryStateSuccess = "success"
+	authEntryStateTimeout = "timeout"
+	authEntryStateError   = "error"
+)
 
-	return sb.String()
+type authEntry struct {
+	State       authEntryState `json:"state"`
+	Error       string         `json:"error,omitempty"`
+	Active      bool           `json:"active"`
+	Host        string         `json:"host"`
+	Login       string         `json:"login"`
+	TokenSource string         `json:"tokenSource"`
+	Token       string         `json:"token,omitempty"`
+	Scopes      string         `json:"scopes,omitempty"`
+	GitProtocol string         `json:"gitProtocol"`
 }
 
-type invalidTokenEntry struct {
-	active           bool
-	host             string
-	user             string
-	tokenSource      string
-	tokenIsWriteable bool
+type authStatus struct {
+	Hosts map[string][]authEntry `json:"hosts"`
 }
 
-func (e invalidTokenEntry) String(cs *iostreams.ColorScheme) string {
-	var sb strings.Builder
-
-	if e.user != "" {
-		sb.WriteString(fmt.Sprintf("  %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.host, cs.Bold(e.user), e.tokenSource))
-	} else {
-		sb.WriteString(fmt.Sprintf("  %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.host, e.tokenSource))
-	}
-	activeStr := fmt.Sprintf("%v", e.active)
-	sb.WriteString(fmt.Sprintf("  - Active account: %s\n", cs.Bold(activeStr)))
-	sb.WriteString(fmt.Sprintf("  - The token in %s is invalid.\n", e.tokenSource))
-	if e.tokenIsWriteable {
-		loginInstructions := fmt.Sprintf("gh auth login -h %s", e.host)
-		logoutInstructions := fmt.Sprintf("gh auth logout -h %s -u %s", e.host, e.user)
-		sb.WriteString(fmt.Sprintf("  - To re-authenticate, run: %s\n", cs.Bold(loginInstructions)))
-		sb.WriteString(fmt.Sprintf("  - To forget about this account, run: %s\n", cs.Bold(logoutInstructions)))
+func newAuthStatus() *authStatus {
+	return &authStatus{
+		Hosts: make(map[string][]authEntry),
 	}
+}
 
-	return sb.String()
+var authStatusFields = []string{
+	"hosts",
 }
 
-type timeoutErrorEntry struct {
-	active      bool
-	host        string
-	user        string
-	tokenSource string
+func (a authStatus) ExportData(fields []string) map[string]interface{} {
+	return cmdutil.StructExportData(a, fields)
 }
 
-func (e timeoutErrorEntry) String(cs *iostreams.ColorScheme) string {
+func (e authEntry) String(cs *iostreams.ColorScheme) string {
 	var sb strings.Builder
 
-	if e.user != "" {
-		sb.WriteString(fmt.Sprintf("  %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.host, cs.Bold(e.user), e.tokenSource))
-	} else {
-		sb.WriteString(fmt.Sprintf("  %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.host, e.tokenSource))
-	}
-	activeStr := fmt.Sprintf("%v", e.active)
-	sb.WriteString(fmt.Sprintf("  - Active account: %s\n", cs.Bold(activeStr)))
-
-	return sb.String()
-}
-
-type Entry interface {
-	String(cs *iostreams.ColorScheme) string
-}
+	switch e.State {
+	case authEntryStateSuccess:
+		sb.WriteString(
+			fmt.Sprintf("  %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource),
+		)
+		activeStr := fmt.Sprintf("%v", e.Active)
+		sb.WriteString(fmt.Sprintf("  - Active account: %s\n", cs.Bold(activeStr)))
+		sb.WriteString(fmt.Sprintf("  - Git operations protocol: %s\n", cs.Bold(e.GitProtocol)))
+		sb.WriteString(fmt.Sprintf("  - Token: %s\n", cs.Bold(e.Token)))
+
+		if expectScopes(e.Token) {
+			sb.WriteString(fmt.Sprintf("  - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes))))
+			if err := shared.HeaderHasMinimumScopes(e.Scopes); err != nil {
+				var missingScopesError *shared.MissingScopesError
+				if errors.As(err, &missingScopesError) {
+					missingScopes := strings.Join(missingScopesError.MissingScopes, ",")
+					sb.WriteString(fmt.Sprintf("  %s Missing required token scopes: %s\n",
+						cs.WarningIcon(),
+						cs.Bold(displayScopes(missingScopes))))
+					refreshInstructions := fmt.Sprintf("gh auth refresh -h %s", e.Host)
+					sb.WriteString(fmt.Sprintf("  - To request missing scopes, run: %s\n", cs.Bold(refreshInstructions)))
+				}
+			}
+		}
 
-type Entries []Entry
+	case authEntryStateError:
+		if e.Login != "" {
+			sb.WriteString(fmt.Sprintf("  %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource))
+		} else {
+			sb.WriteString(fmt.Sprintf("  %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource))
+		}
+		activeStr := fmt.Sprintf("%v", e.Active)
+		sb.WriteString(fmt.Sprintf("  - Active account: %s\n", cs.Bold(activeStr)))
+		sb.WriteString(fmt.Sprintf("  - The token in %s is invalid.\n", e.TokenSource))
+		if authTokenWriteable(e.TokenSource) {
+			loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host)
+			logoutInstructions := fmt.Sprintf("gh auth logout -h %s -u %s", e.Host, e.Login)
+			sb.WriteString(fmt.Sprintf("  - To re-authenticate, run: %s\n", cs.Bold(loginInstructions)))
+			sb.WriteString(fmt.Sprintf("  - To forget about this account, run: %s\n", cs.Bold(logoutInstructions)))
+		}
 
-func (e Entries) Strings(cs *iostreams.ColorScheme) []string {
-	var out []string
-	for _, entry := range e {
-		out = append(out, entry.String(cs))
+	case authEntryStateTimeout:
+		if e.Login != "" {
+			sb.WriteString(fmt.Sprintf("  %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource))
+		} else {
+			sb.WriteString(fmt.Sprintf("  %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource))
+		}
+		activeStr := fmt.Sprintf("%v", e.Active)
+		sb.WriteString(fmt.Sprintf("  - Active account: %s\n", cs.Bold(activeStr)))
 	}
-	return out
+
+	return sb.String()
 }
 
 type StatusOptions struct {
 	HttpClient func() (*http.Client, error)
 	IO         *iostreams.IOStreams
 	Config     func() (gh.Config, error)
+	Exporter   cmdutil.Exporter
 
 	Hostname  string
 	ShowToken bool
@@ -148,11 +141,32 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
 
 			For each host, the authentication state of each known account is tested and any issues are included in the output.
 			Each host section will indicate the active account, which will be used when targeting that host.
+
 			If an account on any host (or only the one given via %[1]s--hostname%[1]s) has authentication issues,
-			the command will exit with 1 and output to stderr.
+			the command will exit with 1 and output to stderr. Note that when using the %[1]s--json%[1]s option, the command
+			will always exit with zero regardless of any authentication issues, unless there is a fatal error.
 
 			To change the active account for a host, see %[1]sgh auth switch%[1]s.
 		`, "`"),
+		Example: heredoc.Doc(`
+			# Display authentication status for all accounts on all hosts
+			$ gh auth status
+
+			# Display authentication status for the active account on a specific host
+			$ gh auth status --active --hostname github.example.com
+			
+			# Display tokens in plain text
+			$ gh auth status --show-token
+
+			# Format authentication status as JSON
+			$ gh auth status --json hosts
+
+			# Include plain text token in JSON output
+			$ gh auth status --json hosts --show-token
+
+			# Format hosts as a flat JSON array
+			$ gh auth status --json hosts --jq '.hosts | add'
+		`),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if runF != nil {
 				return runF(opts)
@@ -166,6 +180,9 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
 	cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "t", false, "Display the auth token")
 	cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only")
 
+	// the json flags are intentionally not given a shorthand to avoid conflict with -t/--show-token
+	cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authStatusFields)
+
 	return cmd
 }
 
@@ -180,18 +197,26 @@ func statusRun(opts *StatusOptions) error {
 	stdout := opts.IO.Out
 	cs := opts.IO.ColorScheme()
 
-	statuses := make(map[string]Entries)
-
 	hostnames := authCfg.Hosts()
 	if len(hostnames) == 0 {
 		fmt.Fprintf(stderr,
 			"You are not logged into any GitHub hosts. To log in, run: %s\n", cs.Bold("gh auth login"))
+		if opts.Exporter != nil {
+			// In machine-friendly mode, we always exit with no error.
+			opts.Exporter.Write(opts.IO, newAuthStatus())
+			return nil
+		}
 		return cmdutil.SilentError
 	}
 
 	if opts.Hostname != "" && !slices.Contains(hostnames, opts.Hostname) {
 		fmt.Fprintf(stderr,
 			"You are not logged into any accounts on %s\n", opts.Hostname)
+		if opts.Exporter != nil {
+			// In machine-friendly mode, we always exit with no error.
+			opts.Exporter.Write(opts.IO, newAuthStatus())
+			return nil
+		}
 		return cmdutil.SilentError
 	}
 
@@ -200,6 +225,9 @@ func statusRun(opts *StatusOptions) error {
 		return err
 	}
 
+	var finalErr error
+	statuses := newAuthStatus()
+
 	for _, hostname := range hostnames {
 		if opts.Hostname != "" && opts.Hostname != hostname {
 			continue
@@ -215,15 +243,14 @@ func statusRun(opts *StatusOptions) error {
 			active:      true,
 			gitProtocol: gitProtocol,
 			hostname:    hostname,
-			showToken:   opts.ShowToken,
 			token:       activeUserToken,
 			tokenSource: activeUserTokenSource,
 			username:    activeUser,
 		})
-		statuses[hostname] = append(statuses[hostname], entry)
+		statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry)
 
-		if err == nil && !isValidEntry(entry) {
-			err = cmdutil.SilentError
+		if finalErr == nil && entry.State != authEntryStateSuccess {
+			finalErr = cmdutil.SilentError
 		}
 
 		if opts.Active {
@@ -240,28 +267,46 @@ func statusRun(opts *StatusOptions) error {
 				active:      false,
 				gitProtocol: gitProtocol,
 				hostname:    hostname,
-				showToken:   opts.ShowToken,
 				token:       token,
 				tokenSource: tokenSource,
 				username:    username,
 			})
-			statuses[hostname] = append(statuses[hostname], entry)
+			statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry)
+
+			if finalErr == nil && entry.State != authEntryStateSuccess {
+				finalErr = cmdutil.SilentError
+			}
+		}
+	}
 
-			if err == nil && !isValidEntry(entry) {
-				err = cmdutil.SilentError
+	if !opts.ShowToken {
+		for _, host := range statuses.Hosts {
+			for i := range host {
+				if opts.Exporter != nil {
+					// In machine-readable we just drop the token
+					host[i].Token = ""
+				} else {
+					host[i].Token = maskToken(host[i].Token)
+				}
 			}
 		}
 	}
 
+	if opts.Exporter != nil {
+		// In machine-friendly mode, we always exit with no error.
+		opts.Exporter.Write(opts.IO, statuses)
+		return nil
+	}
+
 	prevEntry := false
 	for _, hostname := range hostnames {
-		entries, ok := statuses[hostname]
+		entries, ok := statuses.Hosts[hostname]
 		if !ok {
 			continue
 		}
 
 		stream := stdout
-		if err != nil {
+		if finalErr != nil {
 			stream = stderr
 		}
 
@@ -270,22 +315,22 @@ func statusRun(opts *StatusOptions) error {
 		}
 		prevEntry = true
 		fmt.Fprintf(stream, "%s\n", cs.Bold(hostname))
-		fmt.Fprintf(stream, "%s", strings.Join(entries.Strings(cs), "\n"))
+		for i, entry := range entries {
+			fmt.Fprintf(stream, "%s", entry.String(cs))
+			if i < len(entries)-1 {
+				fmt.Fprint(stream, "\n")
+			}
+		}
 	}
 
-	return err
+	return finalErr
 }
 
-func displayToken(token string, printRaw bool) string {
-	if printRaw {
-		return token
-	}
-
+func maskToken(token string) string {
 	if idx := strings.LastIndexByte(token, '_'); idx > -1 {
 		prefix := token[0 : idx+1]
 		return prefix + strings.Repeat("*", len(token)-len(prefix))
 	}
-
 	return strings.Repeat("*", len(token))
 }
 
@@ -308,37 +353,39 @@ type buildEntryOptions struct {
 	active      bool
 	gitProtocol string
 	hostname    string
-	showToken   bool
 	token       string
 	tokenSource string
 	username    string
 }
 
-func buildEntry(httpClient *http.Client, opts buildEntryOptions) Entry {
-	tokenIsWriteable := authTokenWriteable(opts.tokenSource)
-
-	if opts.tokenSource == "oauth_token" {
+func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry {
+	tokenSource := opts.tokenSource
+	if tokenSource == "oauth_token" {
 		// The go-gh function TokenForHost returns this value as source for tokens read from the
 		// config file, but we want the file path instead. This attempts to reconstruct it.
-		opts.tokenSource = filepath.Join(config.ConfigDir(), "hosts.yml")
+		tokenSource = filepath.Join(config.ConfigDir(), "hosts.yml")
+	}
+	entry := authEntry{
+		Active:      opts.active,
+		Host:        opts.hostname,
+		Login:       opts.username,
+		TokenSource: tokenSource,
+		Token:       opts.token,
+		GitProtocol: opts.gitProtocol,
 	}
 
 	// If token is not writeable, then it came from an environment variable and
 	// we need to fetch the username as it won't be stored in the config.
-	if !tokenIsWriteable {
+	if !authTokenWriteable(tokenSource) {
 		// The httpClient will automatically use the correct token here as
 		// the token from the environment variable take highest precedence.
 		apiClient := api.NewClientFromHTTP(httpClient)
 		var err error
-		opts.username, err = api.CurrentLoginName(apiClient, opts.hostname)
+		entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname)
 		if err != nil {
-			return invalidTokenEntry{
-				active:           opts.active,
-				host:             opts.hostname,
-				user:             opts.username,
-				tokenIsWriteable: tokenIsWriteable,
-				tokenSource:      opts.tokenSource,
-			}
+			entry.State = authEntryStateError
+			entry.Error = err.Error()
+			return entry
 		}
 	}
 
@@ -347,39 +394,21 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) Entry {
 	if err != nil {
 		var networkError net.Error
 		if errors.As(err, &networkError) && networkError.Timeout() {
-			return timeoutErrorEntry{
-				active:      opts.active,
-				host:        opts.hostname,
-				user:        opts.username,
-				tokenSource: opts.tokenSource,
-			}
+			entry.State = authEntryStateTimeout
+			entry.Error = err.Error()
+			return entry
 		}
 
-		return invalidTokenEntry{
-			active:           opts.active,
-			host:             opts.hostname,
-			user:             opts.username,
-			tokenIsWriteable: tokenIsWriteable,
-			tokenSource:      opts.tokenSource,
-		}
+		entry.State = authEntryStateError
+		entry.Error = err.Error()
+		return entry
 	}
+	entry.Scopes = scopesHeader
 
-	return validEntry{
-		active:      opts.active,
-		gitProtocol: opts.gitProtocol,
-		host:        opts.hostname,
-		scopes:      scopesHeader,
-		token:       displayToken(opts.token, opts.showToken),
-		tokenSource: opts.tokenSource,
-		user:        opts.username,
-	}
+	entry.State = authEntryStateSuccess
+	return entry
 }
 
 func authTokenWriteable(src string) bool {
 	return !strings.HasSuffix(src, "_TOKEN")
 }
-
-func isValidEntry(entry Entry) bool {
-	_, ok := entry.(validEntry)
-	return ok
-}
diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go
index ab76368ad..58281d831 100644
--- a/pkg/cmd/cache/delete/delete.go
+++ b/pkg/cmd/cache/delete/delete.go
@@ -25,6 +25,7 @@ type DeleteOptions struct {
 	DeleteAll         bool
 	SucceedOnNoCaches bool
 	Identifier        string
+	Ref               string
 }
 
 func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
@@ -51,6 +52,12 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
 			# Delete a cache by id in a specific repo
 			$ gh cache delete 1234 --repo cli/cli
 
+			# Delete a cache by key and branch ref
+			$ gh cache delete cache-key --ref refs/heads/feature-branch
+
+			# Delete a cache by key and PR ref
+			$ gh cache delete cache-key --ref refs/pull/<PR-number>/merge
+
 			# Delete all caches (exit code 1 on no caches)
 			$ gh cache delete --all
 
@@ -69,14 +76,31 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
 				return err
 			}
 
+			if err := cmdutil.MutuallyExclusive(
+				"--ref cannot be used with --all",
+				opts.DeleteAll, opts.Ref != "",
+			); err != nil {
+				return err
+			}
+
 			if !opts.DeleteAll && opts.SucceedOnNoCaches {
 				return cmdutil.FlagErrorf("--succeed-on-no-caches must be used in conjunction with --all")
 			}
 
+			if opts.Ref != "" && len(args) == 0 {
+				return cmdutil.FlagErrorf("must provide a cache key")
+			}
+
 			if !opts.DeleteAll && len(args) == 0 {
 				return cmdutil.FlagErrorf("must provide either cache id, cache key, or use --all")
 			}
 
+			if len(args) > 0 && opts.Ref != "" {
+				if _, ok := parseCacheID(args[0]); ok {
+					return cmdutil.FlagErrorf("--ref cannot be used with cache ID")
+				}
+			}
+
 			if len(args) == 1 {
 				opts.Identifier = args[0]
 			}
@@ -90,6 +114,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
 	}
 
 	cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches")
+	cmd.Flags().StringVarP(&opts.Ref, "ref", "r", "", "Delete by cache key and ref, formatted as refs/heads/<branch name> or refs/pull/<number>/merge")
 	cmd.Flags().BoolVar(&opts.SucceedOnNoCaches, "succeed-on-no-caches", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`")
 
 	return cmd
@@ -142,11 +167,27 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface
 	base := fmt.Sprintf("repos/%s/actions/caches", repoName)
 
 	for _, cache := range toDelete {
+		// TODO(babakks): We use two different endpoints here which have different
+		// response schemas:
+		//
+		//   1. /repos/OWNER/REPO/actions/caches/ID (for deleting by cache ID)
+		//      - returns HTTP 204 (NO CONTENT) on success
+		//   2. /repos/OWNER/REPO/actions/caches?key=KEY[&ref=REF] (for deleting by cache key, and optionally a ref)
+		//      - returns HTTP 200 on success including information about the deleted caches
+		//
+		// So, if/when we decided to use the data in the response body we need
+		// to be careful with parsing. Probably want to split these API calls
+		// into separate functions.
+
 		path := ""
-		if id, err := strconv.Atoi(cache); err == nil {
+		if id, ok := parseCacheID(cache); ok {
 			path = fmt.Sprintf("%s/%d", base, id)
 		} else {
 			path = fmt.Sprintf("%s?key=%s", base, url.QueryEscape(cache))
+
+			if opts.Ref != "" {
+				path += fmt.Sprintf("&ref=%s", url.QueryEscape(opts.Ref))
+			}
 		}
 
 		err := client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
@@ -154,7 +195,11 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface
 			var httpErr api.HTTPError
 			if errors.As(err, &httpErr) {
 				if httpErr.StatusCode == http.StatusNotFound {
-					err = fmt.Errorf("%s Could not find a cache matching %s in %s", cs.FailureIcon(), cache, repoName)
+					if opts.Ref == "" {
+						err = fmt.Errorf("%s Could not find a cache matching %s in %s", cs.FailureIcon(), cache, repoName)
+					} else {
+						err = fmt.Errorf("%s Could not find a cache matching %s (with ref %s) in %s", cs.FailureIcon(), cache, opts.Ref, repoName)
+					}
 				} else {
 					err = fmt.Errorf("%s Failed to delete cache: %w", cs.FailureIcon(), err)
 				}
@@ -172,3 +217,8 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface
 
 	return nil
 }
+
+func parseCacheID(arg string) (int, bool) {
+	id, err := strconv.Atoi(arg)
+	return id, err == nil
+}
diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go
index 354a91cc3..057f91140 100644
--- a/pkg/cmd/extension/command.go
+++ b/pkg/cmd/extension/command.go
@@ -12,6 +12,7 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/git"
+	"github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/tableprinter"
 	"github.com/cli/cli/v2/internal/text"
@@ -164,7 +165,8 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
 					query.Qualifiers = qualifiers
 
 					host, _ := cfg.Authentication().DefaultHost()
-					searcher := search.NewSearcher(client, host)
+					detector := featuredetection.NewDetector(client, host)
+					searcher := search.NewSearcher(client, host, detector)
 
 					if webMode {
 						url := searcher.URL(query)
@@ -507,7 +509,8 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
 						return err
 					}
 
-					searcher := search.NewSearcher(api.NewCachedHTTPClient(client, time.Hour*24), host)
+					detector := featuredetection.NewDetector(client, host)
+					searcher := search.NewSearcher(api.NewCachedHTTPClient(client, time.Hour*24), host, detector)
 
 					gc.Stderr = gio.Discard
 
diff --git a/pkg/cmd/issue/list/http.go b/pkg/cmd/issue/list/http.go
index fcbfe7240..0fceb3e97 100644
--- a/pkg/cmd/issue/list/http.go
+++ b/pkg/cmd/issue/list/http.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/cli/cli/v2/api"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
 )
@@ -112,7 +113,16 @@ loop:
 	return &res, nil
 }
 
-func searchIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
+func searchIssues(client *api.Client, detector fd.Detector, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
+	// TODO advancedIssueSearchCleanup
+	// We won't need feature detection when GHES 3.17 support ends, since
+	// the advanced issue search is the only available search backend for
+	// issues.
+	features, err := detector.SearchFeatures()
+	if err != nil {
+		return nil, err
+	}
+
 	fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.IssueGraphQL(filters.Fields))
 	query := fragments +
 		`query IssueSearch($repo: String!, $owner: String!, $type: SearchType!, $limit: Int, $after: String, $query: String!) {
@@ -143,18 +153,27 @@ func searchIssues(client *api.Client, repo ghrepo.Interface, filters prShared.Fi
 		}
 	}
 
-	filters.Repo = ghrepo.FullName(repo)
-	filters.Entity = "issue"
-	q := prShared.SearchQueryBuild(filters)
-
 	perPage := min(limit, 100)
 
 	variables := map[string]interface{}{
 		"owner": repo.RepoOwner(),
 		"repo":  repo.RepoName(),
-		"type":  "ISSUE",
 		"limit": perPage,
-		"query": q,
+	}
+
+	filters.Repo = ghrepo.FullName(repo)
+	filters.Entity = "issue"
+
+	if features.AdvancedIssueSearchAPI {
+		variables["query"] = prShared.SearchQueryBuild(filters, true)
+		if features.AdvancedIssueSearchAPIOptIn {
+			variables["type"] = "ISSUE_ADVANCED"
+		} else {
+			variables["type"] = "ISSUE"
+		}
+	} else {
+		variables["query"] = prShared.SearchQueryBuild(filters, false)
+		variables["type"] = "ISSUE"
 	}
 
 	ic := api.IssuesAndTotalCount{SearchCapped: limit > 1000}
diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go
index 46c0e2cb0..9a6495ec2 100644
--- a/pkg/cmd/issue/list/list.go
+++ b/pkg/cmd/issue/list/list.go
@@ -58,12 +58,18 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 	cmd := &cobra.Command{
 		Use:   "list",
 		Short: "List issues in a repository",
-		Long: heredoc.Doc(`
+		// TODO advancedIssueSearchCleanup
+		// Update the links and remove the mention at GHES 3.17 version.
+		Long: heredoc.Docf(`
 			List issues in a GitHub repository. By default, this only lists open issues.
 
 			The search query syntax is documented here:
 			<https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests>
-		`),
+
+			On supported GitHub hosts, advanced issue search syntax can be used in the
+			%[1]s--search%[1]s query. For more information about advanced issue search, see:
+			<https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/filtering-and-searching-issues-and-pull-requests#building-advanced-filters-for-issues>
+		`, "`"),
 		Example: heredoc.Doc(`
 			$ gh issue list --label "bug" --label "help wanted"
 			$ gh issue list --author monalisa
@@ -165,8 +171,21 @@ func listRun(opts *ListOptions) error {
 	isTerminal := opts.IO.IsStdoutTTY()
 
 	if opts.WebMode {
+		// TODO advancedIssueSearchCleanup
+		// We won't need feature detection when GHES 3.17 support ends, since
+		// the advanced issue search is the only available search backend for
+		// issues, and the GUI (i.e. Issues tab of repos) already supports the
+		// advanced syntax.
+		searchFeatures, err := opts.Detector.SearchFeatures()
+		if err != nil {
+			return err
+		}
+
 		issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues")
-		openURL, err := prShared.ListURLWithQuery(issueListURL, filterOptions)
+
+		// Note that if the advanced issue search API is available, the syntax is
+		// also supported in the Issues tab.
+		openURL, err := prShared.ListURLWithQuery(issueListURL, filterOptions, searchFeatures.AdvancedIssueSearchAPI)
 		if err != nil {
 			return err
 		}
@@ -181,7 +200,7 @@ func listRun(opts *ListOptions) error {
 		filterOptions.Fields = opts.Exporter.Fields()
 	}
 
-	listResult, err := issueList(httpClient, baseRepo, filterOptions, opts.LimitResults)
+	listResult, err := issueList(httpClient, opts.Detector, baseRepo, filterOptions, opts.LimitResults)
 	if err != nil {
 		return err
 	}
@@ -212,7 +231,7 @@ func listRun(opts *ListOptions) error {
 	return nil
 }
 
-func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
+func issueList(client *http.Client, detector fd.Detector, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) {
 	apiClient := api.NewClientFromHTTP(client)
 
 	if filters.Search != "" || len(filters.Labels) > 0 || filters.Milestone != "" {
@@ -224,7 +243,7 @@ func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.Filt
 			filters.Milestone = milestone.Title
 		}
 
-		return searchIssues(apiClient, repo, filters, limit)
+		return searchIssues(apiClient, detector, repo, filters, limit)
 	}
 
 	var err error
diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go
index 32189fda1..a137f92f5 100644
--- a/pkg/cmd/pr/checkout/checkout.go
+++ b/pkg/cmd/pr/checkout/checkout.go
@@ -57,7 +57,8 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
 			$ gh pr checkout https://github.com/OWNER/REPO/pull/32
 			$ gh pr checkout feature
 		`),
-		Args: cobra.MaximumNArgs(1),
+		Args:    cobra.MaximumNArgs(1),
+		Aliases: []string{"co"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) > 0 {
 				opts.PRResolver = &specificPRResolver{
diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go
index c01364d24..b196546cd 100644
--- a/pkg/cmd/pr/edit/edit.go
+++ b/pkg/cmd/pr/edit/edit.go
@@ -97,7 +97,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
 				// needs to know the API host. If the command is run outside of
 				// a git repo, we cannot instantiate the detector unless we have
 				// already parsed the URL.
-				if baseRepo, _, err := shared.ParseURL(opts.SelectorArg); err == nil {
+				if baseRepo, _, _, err := shared.ParseURL(opts.SelectorArg); err == nil {
 					opts.BaseRepo = func() (ghrepo.Interface, error) {
 						return baseRepo, nil
 					}
diff --git a/pkg/cmd/pr/list/http.go b/pkg/cmd/pr/list/http.go
index 4c69af708..4f82d5006 100644
--- a/pkg/cmd/pr/list/http.go
+++ b/pkg/cmd/pr/list/http.go
@@ -5,6 +5,7 @@ import (
 	"net/http"
 
 	"github.com/cli/cli/v2/api"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
 )
@@ -13,9 +14,9 @@ func shouldUseSearch(filters prShared.FilterOptions) bool {
 	return filters.Draft != nil || filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0
 }
 
-func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
+func listPullRequests(httpClient *http.Client, detector fd.Detector, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
 	if shouldUseSearch(filters) {
-		return searchPullRequests(httpClient, repo, filters, limit)
+		return searchPullRequests(httpClient, detector, repo, filters, limit)
 	}
 
 	return prShared.NewLister(httpClient).List(prShared.ListOptions{
@@ -28,7 +29,16 @@ func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters pr
 	})
 }
 
-func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
+func searchPullRequests(httpClient *http.Client, detector fd.Detector, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
+	// TODO advancedIssueSearchCleanup
+	// We won't need feature detection when GHES 3.17 support ends, since
+	// the advanced issue search is the only available search backend for
+	// issues.
+	features, err := detector.SearchFeatures()
+	if err != nil {
+		return nil, err
+	}
+
 	type response struct {
 		Search struct {
 			Nodes    []api.PullRequest
@@ -44,10 +54,11 @@ func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters
 	query := fragment + `
 		query PullRequestSearch(
 			$q: String!,
+			$type: SearchType!,
 			$limit: Int!,
 			$endCursor: String,
 		) {
-			search(query: $q, type: ISSUE, first: $limit, after: $endCursor) {
+			search(query: $q, type: $type, first: $limit, after: $endCursor) {
 				issueCount
 				nodes {
 					...pr
@@ -59,12 +70,24 @@ func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters
 			}
 		}`
 
+	variables := map[string]interface{}{}
+
 	filters.Repo = ghrepo.FullName(repo)
 	filters.Entity = "pr"
-	q := prShared.SearchQueryBuild(filters)
+
+	if features.AdvancedIssueSearchAPI {
+		variables["q"] = prShared.SearchQueryBuild(filters, true)
+		if features.AdvancedIssueSearchAPIOptIn {
+			variables["type"] = "ISSUE_ADVANCED"
+		} else {
+			variables["type"] = "ISSUE"
+		}
+	} else {
+		variables["q"] = prShared.SearchQueryBuild(filters, false)
+		variables["type"] = "ISSUE"
+	}
 
 	pageLimit := min(limit, 100)
-	variables := map[string]interface{}{"q": q}
 
 	res := api.PullRequestAndTotalCount{SearchCapped: limit > 1000}
 	var check = make(map[int]struct{})
diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go
index 7188df1a5..a7e40384a 100644
--- a/pkg/cmd/pr/list/list.go
+++ b/pkg/cmd/pr/list/list.go
@@ -10,6 +10,7 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/browser"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/tableprinter"
 	"github.com/cli/cli/v2/internal/text"
@@ -24,6 +25,7 @@ type ListOptions struct {
 	IO         *iostreams.IOStreams
 	BaseRepo   func() (ghrepo.Interface, error)
 	Browser    browser.Browser
+	Detector   fd.Detector
 
 	WebMode      bool
 	LimitResults int
@@ -54,12 +56,18 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 	cmd := &cobra.Command{
 		Use:   "list",
 		Short: "List pull requests in a repository",
-		Long: heredoc.Doc(`
+		// TODO advancedIssueSearchCleanup
+		// Update the links and remove the mention at GHES 3.17 version.
+		Long: heredoc.Docf(`
 			List pull requests in a GitHub repository. By default, this only lists open PRs.
 
 			The search query syntax is documented here:
 			<https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests>
-		`),
+			
+			On supported GitHub hosts, advanced issue search syntax can be used in the
+			%[1]s--search%[1]s query. For more information about advanced issue search, see:
+			<https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/filtering-and-searching-issues-and-pull-requests#building-advanced-filters-for-issues>
+		`, "`"),
 		Example: heredoc.Doc(`
 			# List PRs authored by you
 			$ gh pr list --author "@me"
@@ -142,6 +150,11 @@ func listRun(opts *ListOptions) error {
 		return err
 	}
 
+	if opts.Detector == nil {
+		cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
+		opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
+	}
+
 	prState := strings.ToLower(opts.State)
 	if prState == "open" && shared.QueryHasStateClause(opts.Search) {
 		prState = ""
@@ -164,7 +177,12 @@ func listRun(opts *ListOptions) error {
 	}
 	if opts.WebMode {
 		prListURL := ghrepo.GenerateRepoURL(baseRepo, "pulls")
-		openURL, err := shared.ListURLWithQuery(prListURL, filters)
+
+		// TODO advancedSearchFuture
+		// As of August 2025, the advanced issue search syntax is not supported
+		// in Pull Requests tab of repositories. When it's supported we can
+		// change the argument to true.
+		openURL, err := shared.ListURLWithQuery(prListURL, filters, false)
 		if err != nil {
 			return err
 		}
@@ -175,7 +193,7 @@ func listRun(opts *ListOptions) error {
 		return opts.Browser.Browse(openURL)
 	}
 
-	listResult, err := listPullRequests(httpClient, baseRepo, filters, opts.LimitResults)
+	listResult, err := listPullRequests(httpClient, opts.Detector, baseRepo, filters, opts.LimitResults)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go
index df8cc0fd4..1879fff62 100644
--- a/pkg/cmd/pr/shared/finder.go
+++ b/pkg/cmd/pr/shared/finder.go
@@ -103,6 +103,8 @@ type FindOptions struct {
 	// States lists the possible PR states to scope the PR-for-branch lookup to.
 	States []string
 
+	DisableProgress bool
+
 	Detector fd.Detector
 }
 
@@ -112,7 +114,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 		return nil, nil, errors.New("Find error: no fields specified")
 	}
 
-	if repo, prNumber, err := ParseURL(opts.Selector); err == nil {
+	if repo, prNumber, _, err := ParseURL(opts.Selector); err == nil {
 		f.prNumber = prNumber
 		f.baseRefRepo = repo
 	}
@@ -196,8 +198,9 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 		return nil, nil, err
 	}
 
+	// TODO: Decouple the PR finder from IO
 	// TODO(josebalius): Should we be guarding here?
-	if f.progress != nil {
+	if !opts.DisableProgress && f.progress != nil {
 		f.progress.StartProgressIndicator()
 		defer f.progress.StopProgressIndicator()
 	}
@@ -300,32 +303,59 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 	return pr, f.baseRefRepo, g.Wait()
 }
 
-var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`)
+var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)(.*$)`)
 
-// ParseURL parses a pull request URL and returns the repository and pull
-// request number.
-func ParseURL(prURL string) (ghrepo.Interface, int, error) {
+// ParseURL parses a pull request URL and returns the repository, pull request
+// number, and any tailing path components. If there is no error, the returned
+// repo is not nil and will have non-empty hostname.
+func ParseURL(prURL string) (ghrepo.Interface, int, string, error) {
 	if prURL == "" {
-		return nil, 0, fmt.Errorf("invalid URL: %q", prURL)
+		return nil, 0, "", fmt.Errorf("invalid URL: %q", prURL)
 	}
 
 	u, err := url.Parse(prURL)
 	if err != nil {
-		return nil, 0, err
+		return nil, 0, "", err
 	}
 
 	if u.Scheme != "https" && u.Scheme != "http" {
-		return nil, 0, fmt.Errorf("invalid scheme: %s", u.Scheme)
+		return nil, 0, "", fmt.Errorf("invalid scheme: %s", u.Scheme)
 	}
 
 	m := pullURLRE.FindStringSubmatch(u.Path)
 	if m == nil {
-		return nil, 0, fmt.Errorf("not a pull request URL: %s", prURL)
+		return nil, 0, "", fmt.Errorf("not a pull request URL: %s", prURL)
 	}
 
 	repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
 	prNumber, _ := strconv.Atoi(m[3])
-	return repo, prNumber, nil
+	tail := m[4]
+	return repo, prNumber, tail, nil
+}
+
+var fullReferenceRE = regexp.MustCompile(`^(?:([^/]+)/([^/]+))#(\d+)$`)
+
+// ParseFullReference parses a short issue/pull request reference of the form
+// "owner/repo#number", where owner, repo and number are all required.
+func ParseFullReference(s string) (ghrepo.Interface, int, error) {
+	if s == "" {
+		return nil, 0, errors.New("empty reference")
+	}
+
+	m := fullReferenceRE.FindStringSubmatch(s)
+	if m == nil {
+		return nil, 0, fmt.Errorf("invalid reference: %q", s)
+	}
+
+	number, err := strconv.Atoi(m[3])
+	if err != nil {
+		return nil, 0, fmt.Errorf("invalid reference: %q", number)
+	}
+
+	owner := m[1]
+	repo := m[2]
+
+	return ghrepo.New(owner, repo), number, nil
 }
 
 func findByNumber(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.PullRequest, error) {
diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go
index c3315aeae..9504ecef4 100644
--- a/pkg/cmd/pr/shared/params.go
+++ b/pkg/cmd/pr/shared/params.go
@@ -186,20 +186,20 @@ func (opts *FilterOptions) IsDefault() bool {
 	return true
 }
 
-func ListURLWithQuery(listURL string, options FilterOptions) (string, error) {
+func ListURLWithQuery(listURL string, options FilterOptions, advancedIssueSearchSyntax bool) (string, error) {
 	u, err := url.Parse(listURL)
 	if err != nil {
 		return "", err
 	}
 
 	params := u.Query()
-	params.Set("q", SearchQueryBuild(options))
+	params.Set("q", SearchQueryBuild(options, advancedIssueSearchSyntax))
 	u.RawQuery = params.Encode()
 
 	return u.String(), nil
 }
 
-func SearchQueryBuild(options FilterOptions) string {
+func SearchQueryBuild(options FilterOptions, advancedIssueSearchSyntax bool) string {
 	var is, state string
 	switch options.State {
 	case "open", "closed":
@@ -207,7 +207,7 @@ func SearchQueryBuild(options FilterOptions) string {
 	case "merged":
 		is = "merged"
 	}
-	q := search.Query{
+	query := search.Query{
 		Qualifiers: search.Qualifiers{
 			Assignee:  options.Assignee,
 			Author:    options.Author,
@@ -223,10 +223,18 @@ func SearchQueryBuild(options FilterOptions) string {
 			Type:      options.Entity,
 		},
 	}
+
+	var q string
+	if advancedIssueSearchSyntax {
+		q = query.AdvancedIssueSearchString()
+	} else {
+		q = query.StandardSearchString()
+	}
+
 	if options.Search != "" {
-		return fmt.Sprintf("%s %s", options.Search, q.String())
+		return fmt.Sprintf("%s %s", options.Search, q)
 	}
-	return q.String()
+	return q
 }
 
 func QueryHasStateClause(searchQuery string) bool {
diff --git a/pkg/cmd/release/verify-asset/verify_asset.go b/pkg/cmd/release/verify-asset/verify_asset.go
index 700136ca5..cad436eaa 100644
--- a/pkg/cmd/release/verify-asset/verify_asset.go
+++ b/pkg/cmd/release/verify-asset/verify_asset.go
@@ -161,14 +161,15 @@ func verifyAssetRun(config *VerifyAssetConfig) error {
 		// The limit is set to 100 to ensure we fetch all attestations for a given SHA.
 		// While multiple attestations can exist for a single SHA,
 		// only one attestation is associated with each release tag.
-		Limit: 100,
+		Initiator: "github",
+		Limit:     100,
 	})
 	if err != nil {
 		return fmt.Errorf("no attestations found for tag %s (%s)", tagName, releaseRefDigest.DigestWithAlg())
 	}
 
 	// Filter attestations by tag name
-	filteredAttestations, err := shared.FilterAttestationsByTag(attestations, opts.TagName)
+	filteredAttestations, err := shared.FilterAttestationsByTag(attestations, tagName)
 	if err != nil {
 		return fmt.Errorf("error parsing attestations for tag %s: %w", tagName, err)
 	}
@@ -201,9 +202,9 @@ func verifyAssetRun(config *VerifyAssetConfig) error {
 	io := config.IO
 	cs := io.ColorScheme()
 	fmt.Fprintf(io.Out, "Calculated digest for %s: %s\n", fileName, fileDigest.DigestWithAlg())
-	fmt.Fprintf(io.Out, "Resolved tag %s to %s\n", opts.TagName, releaseRefDigest.DigestWithAlg())
+	fmt.Fprintf(io.Out, "Resolved tag %s to %s\n", tagName, releaseRefDigest.DigestWithAlg())
 	fmt.Fprint(io.Out, "Loaded attestation from GitHub API\n\n")
-	fmt.Fprintf(io.Out, cs.Green("%s Verification succeeded! %s is present in release %s\n"), cs.SuccessIcon(), fileName, opts.TagName)
+	fmt.Fprintf(io.Out, cs.Green("%s Verification succeeded! %s is present in release %s\n"), cs.SuccessIcon(), fileName, tagName)
 
 	return nil
 }
diff --git a/pkg/cmd/release/verify/verify.go b/pkg/cmd/release/verify/verify.go
index d71c77c0a..f0b92677a 100644
--- a/pkg/cmd/release/verify/verify.go
+++ b/pkg/cmd/release/verify/verify.go
@@ -143,6 +143,7 @@ func verifyRun(config *VerifyConfig) error {
 		PredicateType: shared.ReleasePredicateType,
 		Owner:         baseRepo.RepoOwner(),
 		Repo:          baseRepo.RepoOwner() + "/" + baseRepo.RepoName(),
+		Initiator:     "github",
 		// TODO: Allow this value to be set via a flag.
 		// The limit is set to 100 to ensure we fetch all attestations for a given SHA.
 		// While multiple attestations can exist for a single SHA,
diff --git a/pkg/cmd/repo/list/http.go b/pkg/cmd/repo/list/http.go
index 0508c6d80..d896c9224 100644
--- a/pkg/cmd/repo/list/http.go
+++ b/pkg/cmd/repo/list/http.go
@@ -213,5 +213,5 @@ func searchQuery(owner string, filter FilterOptions) string {
 		},
 	}
 
-	return q.String()
+	return q.StandardSearchString()
 }
diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go
index 6a709c336..27f028e44 100644
--- a/pkg/cmd/root/root.go
+++ b/pkg/cmd/root/root.go
@@ -8,6 +8,7 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	accessibilityCmd "github.com/cli/cli/v2/pkg/cmd/accessibility"
 	actionsCmd "github.com/cli/cli/v2/pkg/cmd/actions"
+	agentTaskCmd "github.com/cli/cli/v2/pkg/cmd/agent-task"
 	aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
 	"github.com/cli/cli/v2/pkg/cmd/alias/shared"
 	apiCmd "github.com/cli/cli/v2/pkg/cmd/api"
@@ -126,6 +127,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
 	cmd.AddCommand(versionCmd.NewCmdVersion(f, version, buildDate))
 	cmd.AddCommand(accessibilityCmd.NewCmdAccessibility(f))
 	cmd.AddCommand(actionsCmd.NewCmdActions(f))
+	cmd.AddCommand(agentTaskCmd.NewCmdAgentTask(f))
 	cmd.AddCommand(aliasCmd.NewCmdAlias(f))
 	cmd.AddCommand(authCmd.NewCmdAuth(f))
 	cmd.AddCommand(attestationCmd.NewCmdAttestation(f))
diff --git a/pkg/cmd/search/issues/issues.go b/pkg/cmd/search/issues/issues.go
index 1d6ec6428..e616430fb 100644
--- a/pkg/cmd/search/issues/issues.go
+++ b/pkg/cmd/search/issues/issues.go
@@ -26,6 +26,8 @@ func NewCmdIssues(f *cmdutil.Factory, runF func(*shared.IssuesOptions) error) *c
 	cmd := &cobra.Command{
 		Use:   "issues [<query>]",
 		Short: "Search for issues",
+		// TODO advancedIssueSearchCleanup
+		// Update the links and remove the mention at GHES 3.17 version.
 		Long: heredoc.Docf(`
 			Search for issues on GitHub.
 
@@ -35,6 +37,10 @@ func NewCmdIssues(f *cmdutil.Factory, runF func(*shared.IssuesOptions) error) *c
 			GitHub search syntax is documented at:
 			<https://docs.github.com/search-github/searching-on-github/searching-issues-and-pull-requests>
 
+			On supported GitHub hosts, advanced issue search syntax can be used in the
+			%[1]s--search%[1]s query. For more information about advanced issue search, see:
+			<https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/filtering-and-searching-issues-and-pull-requests#building-advanced-filters-for-issues>
+
 			For more information on handling search queries containing a hyphen, run %[1]sgh search --help%[1]s.
 		`, "`"),
 		Example: heredoc.Doc(`
diff --git a/pkg/cmd/search/prs/prs.go b/pkg/cmd/search/prs/prs.go
index 98ab730b5..498569863 100644
--- a/pkg/cmd/search/prs/prs.go
+++ b/pkg/cmd/search/prs/prs.go
@@ -28,6 +28,8 @@ func NewCmdPrs(f *cmdutil.Factory, runF func(*shared.IssuesOptions) error) *cobr
 	cmd := &cobra.Command{
 		Use:   "prs [<query>]",
 		Short: "Search for pull requests",
+		// TODO advancedIssueSearchCleanup
+		// Update the links and remove the mention at GHES 3.17 version.
 		Long: heredoc.Docf(`
 			Search for pull requests on GitHub.
 
@@ -37,6 +39,10 @@ func NewCmdPrs(f *cmdutil.Factory, runF func(*shared.IssuesOptions) error) *cobr
 			GitHub search syntax is documented at:
 			<https://docs.github.com/search-github/searching-on-github/searching-issues-and-pull-requests>
 
+			On supported GitHub hosts, advanced issue search syntax can be used in the
+			%[1]s--search%[1]s query. For more information about advanced issue search, see:
+			<https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/filtering-and-searching-issues-and-pull-requests#building-advanced-filters-for-issues>
+
 			For more information on handling search queries containing a hyphen, run %[1]sgh search --help%[1]s.
 		`, "`"),
 		Example: heredoc.Doc(`
diff --git a/pkg/cmd/search/shared/shared.go b/pkg/cmd/search/shared/shared.go
index 3282599cf..1989bb9d3 100644
--- a/pkg/cmd/search/shared/shared.go
+++ b/pkg/cmd/search/shared/shared.go
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/cli/cli/v2/internal/browser"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/tableprinter"
 	"github.com/cli/cli/v2/internal/text"
 	"github.com/cli/cli/v2/pkg/cmdutil"
@@ -42,12 +43,16 @@ func Searcher(f *cmdutil.Factory) (search.Searcher, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	host, _ := cfg.Authentication().DefaultHost()
 	client, err := f.HttpClient()
 	if err != nil {
 		return nil, err
 	}
-	return search.NewSearcher(client, host), nil
+
+	detector := fd.NewDetector(client, host)
+
+	return search.NewSearcher(client, host, detector), nil
 }
 
 func SearchIssues(opts *IssuesOptions) error {
diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go
index 596c2f216..579d38552 100644
--- a/pkg/cmdutil/json_flags.go
+++ b/pkg/cmdutil/json_flags.go
@@ -25,9 +25,33 @@ type JSONFlagError struct {
 
 func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) {
 	f := cmd.Flags()
+	addJsonFlag(f)
+	addJqFlag(f, "q")
+	addTemplateFlag(f, "t")
+
+	setupJsonFlags(cmd, exportTarget, fields)
+}
+
+func AddJSONFlagsWithoutShorthand(cmd *cobra.Command, exportTarget *Exporter, fields []string) {
+	f := cmd.Flags()
+	addJsonFlag(f)
+	addJqFlag(f, "")
+	addTemplateFlag(f, "")
+
+	setupJsonFlags(cmd, exportTarget, fields)
+}
+
+func addJsonFlag(f *pflag.FlagSet) {
 	f.StringSlice("json", nil, "Output JSON with the specified `fields`")
-	f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`")
-	f.StringP("template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"")
+}
+func addJqFlag(f *pflag.FlagSet, shorthand string) {
+	f.StringP("jq", shorthand, "", "Filter JSON output using a jq `expression`")
+}
+func addTemplateFlag(f *pflag.FlagSet, shorthand string) {
+	f.StringP("template", shorthand, "", "Format JSON output using a Go template; see \"gh help formatting\"")
+}
+
+func setupJsonFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) {
 
 	_ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		var results []string
diff --git a/pkg/search/query.go b/pkg/search/query.go
index c517f90d8..0419e359a 100644
--- a/pkg/search/query.go
+++ b/pkg/search/query.go
@@ -3,7 +3,7 @@ package search
 import (
 	"fmt"
 	"reflect"
-	"sort"
+	"slices"
 	"strings"
 	"unicode"
 )
@@ -86,13 +86,120 @@ type Qualifiers struct {
 	User                []string
 }
 
-func (q Query) String() string {
-	qualifiers := formatQualifiers(q.Qualifiers)
+// String returns the string representation of the query which can be used with
+// the legacy search backend, which is used in global search GUI (i.e.
+// github.com/search), or Pull Requests tab (in repositories). Note that this is
+// a common query format that can be used to search for various entity types
+// (e.g., issues, commits, repositories, etc)
+//
+// With the legacy search backend, the query is made of concatenating keywords
+// and qualifiers with whitespaces. Note that at the backend side, most of the
+// repeated qualifiers are AND-ed, while a handful of qualifiers (i.e.
+// is:private/public, repo:, user:, or in:) are implicitly OR-ed. The legacy
+// search backend does not support the advanced syntax which allows for nested
+// queries and explicit OR operators.
+//
+// At the moment, the advanced search syntax is only available for searching
+// issues, and it's called advanced issue search.
+func (q Query) StandardSearchString() string {
+	qualifiers := formatQualifiers(q.Qualifiers, nil)
 	keywords := formatKeywords(q.Keywords)
 	all := append(keywords, qualifiers...)
 	return strings.TrimSpace(strings.Join(all, " "))
 }
 
+// AdvancedIssueSearchString returns the string representation of the query
+// compatible with the advanced issue search syntax. The query can be used in
+// Issues tab (of repositories) and the Issues dashboard (i.e.
+// github.com/issues).
+//
+// As the name suggests, this query syntax is only supported for searching
+// issues (i.e. issues and PRs). The advanced syntax allows nested queries and
+// explicit OR operators. Unlike the legacy search backend, the advanced issue
+// search does not OR repeated instances of special qualifiers (i.e.
+// is:private/public, repo:, user:, or in:).
+//
+// To keep the gh experience consistent and backward-compatible, the mentioned
+// special qualifiers are explicitly grouped and combined with an OR operator.
+//
+// The advanced syntax is documented at https://github.blog/changelog/2025-03-06-github-issues-projects-api-support-for-issues-advanced-search-and-more
+func (q Query) AdvancedIssueSearchString() string {
+	qualifiers := formatQualifiers(q.Qualifiers, formatAdvancedIssueSearch)
+	keywords := formatKeywords(q.Keywords)
+	all := append(keywords, qualifiers...)
+	return strings.TrimSpace(strings.Join(all, " "))
+}
+
+func formatAdvancedIssueSearch(qualifier string, vs []string) (s []string, applicable bool) {
+	switch qualifier {
+	case "in":
+		return formatSpecialQualifiers("in", vs, [][]string{{"title", "body", "comments"}}), true
+	case "is":
+		return formatSpecialQualifiers("is", vs, [][]string{{"blocked", "blocking"}, {"closed", "open"}, {"issue", "pr"}, {"locked", "unlocked"}, {"merged", "unmerged"}, {"private", "public"}}), true
+	case "user", "repo":
+		return []string{groupWithOR(qualifier, vs)}, true
+	}
+	// Let the default formatting take over
+	return nil, false
+}
+
+func formatSpecialQualifiers(qualifier string, vs []string, specialGroupsToOR [][]string) []string {
+	specialGroups := make([][]string, len(specialGroupsToOR))
+	rest := make([]string, 0, len(vs))
+	for _, v := range vs {
+		var isSpecial bool
+		for i, subValuesToOR := range specialGroupsToOR {
+			if slices.Contains(subValuesToOR, v) {
+				specialGroups[i] = append(specialGroups[i], v)
+				isSpecial = true
+				break
+			}
+		}
+
+		if isSpecial {
+			continue
+		}
+
+		rest = append(rest, v)
+	}
+
+	all := make([]string, 0, len(specialGroups)+len(rest))
+
+	for _, group := range specialGroups {
+		if len(group) == 0 {
+			continue
+		}
+		all = append(all, groupWithOR(qualifier, group))
+	}
+
+	if len(rest) > 0 {
+		for _, v := range rest {
+			all = append(all, fmt.Sprintf("%s:%s", qualifier, quote(v)))
+		}
+	}
+
+	slices.Sort(all)
+	return all
+}
+
+func groupWithOR(qualifier string, vs []string) string {
+	if len(vs) == 0 {
+		return ""
+	}
+
+	all := make([]string, 0, len(vs))
+	for _, v := range vs {
+		all = append(all, fmt.Sprintf("%s:%s", qualifier, quote(v)))
+	}
+
+	if len(all) == 1 {
+		return all[0]
+	}
+
+	slices.Sort(all)
+	return fmt.Sprintf("(%s)", strings.Join(all, " OR "))
+}
+
 func (q Qualifiers) Map() map[string][]string {
 	m := map[string][]string{}
 	v := reflect.ValueOf(q)
@@ -138,15 +245,51 @@ func quote(s string) string {
 	return s
 }
 
-func formatQualifiers(qs Qualifiers) []string {
-	var all []string
+// formatQualifiers renders qualifiers into a plain query.
+//
+// The formatter is a custom formatting function that can be used to modify the
+// output of each qualifier. If the formatter returns (nil, false) the default
+// formatting will be applied.
+func formatQualifiers(qs Qualifiers, formatter func(qualifier string, vs []string) (s []string, applicable bool)) []string {
+	type entry struct {
+		key    string
+		values []string
+	}
+
+	var all []entry
 	for k, vs := range qs.Map() {
+		if len(vs) == 0 {
+			continue
+		}
+
+		e := entry{key: k}
+
+		if formatter != nil {
+			if s, applicable := formatter(k, vs); applicable {
+				e.values = s
+				all = append(all, e)
+				continue
+			}
+		}
+
 		for _, v := range vs {
-			all = append(all, fmt.Sprintf("%s:%s", k, quote(v)))
+			e.values = append(e.values, fmt.Sprintf("%s:%s", k, quote(v)))
 		}
+		if len(e.values) > 1 {
+			slices.Sort(e.values)
+		}
+		all = append(all, e)
 	}
-	sort.Strings(all)
-	return all
+
+	slices.SortFunc(all, func(a, b entry) int {
+		return strings.Compare(a.key, b.key)
+	})
+
+	result := make([]string, 0, len(all))
+	for _, e := range all {
+		result = append(result, e.values...)
+	}
+	return result
 }
 
 func formatKeywords(ks []string) []string {
diff --git a/pkg/search/searcher.go b/pkg/search/searcher.go
index 7cbd35562..5cc94ac9e 100644
--- a/pkg/search/searcher.go
+++ b/pkg/search/searcher.go
@@ -10,6 +10,7 @@ import (
 	"strconv"
 	"strings"
 
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghinstance"
 )
 
@@ -34,8 +35,9 @@ type Searcher interface {
 }
 
 type searcher struct {
-	client *http.Client
-	host   string
+	client   *http.Client
+	detector fd.Detector
+	host     string
 }
 
 type httpError struct {
@@ -52,10 +54,11 @@ type httpErrorItem struct {
 	Resource string
 }
 
-func NewSearcher(client *http.Client, host string) Searcher {
+func NewSearcher(client *http.Client, host string, detector fd.Detector) Searcher {
 	return &searcher{
-		client: client,
-		host:   host,
+		client:   client,
+		host:     host,
+		detector: detector,
 	}
 }
 
@@ -205,7 +208,31 @@ func (s searcher) search(query Query, result interface{}) (*http.Response, error
 	qs := url.Values{}
 	qs.Set("page", strconv.Itoa(query.Page))
 	qs.Set("per_page", strconv.Itoa(query.Limit))
-	qs.Set("q", query.String())
+
+	if query.Kind == KindIssues {
+		// TODO advancedIssueSearchCleanup
+		// We won't need feature detection when GHES 3.17 support ends, since
+		// the advanced issue search is the only available search backend for
+		// issues.
+		features, err := s.detector.SearchFeatures()
+		if err != nil {
+			return nil, err
+		}
+
+		if !features.AdvancedIssueSearchAPI {
+			qs.Set("q", query.StandardSearchString())
+		} else {
+			qs.Set("q", query.AdvancedIssueSearchString())
+
+			if features.AdvancedIssueSearchAPIOptIn {
+				// Advanced syntax should be explicitly enabled
+				qs.Set("advanced_search", "true")
+			}
+		}
+	} else {
+		qs.Set("q", query.StandardSearchString())
+	}
+
 	if query.Order != "" {
 		qs.Set(orderKey, query.Order)
 	}
@@ -240,11 +267,19 @@ func (s searcher) search(query Query, result interface{}) (*http.Response, error
 	return resp, nil
 }
 
+// URL returns URL to the global search in web GUI (i.e. github.com/search).
 func (s searcher) URL(query Query) string {
 	path := fmt.Sprintf("https://%s/search", s.host)
 	qs := url.Values{}
 	qs.Set("type", query.Kind)
-	qs.Set("q", query.String())
+
+	// TODO advancedSearchFuture
+	// Currently, the global search GUI does not support the advanced issue
+	// search syntax (even for the issues/PRs tab on the sidebar). When the GUI
+	// is updated, we can use feature detection, and, if available, use the
+	// advanced search syntax.
+	qs.Set("q", query.StandardSearchString())
+
 	if query.Order != "" {
 		qs.Set(orderKey, query.Order)
 	}
diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md
index 0f84c9ff3..df2abd079 100644
--- a/third-party-licenses.darwin.md
+++ b/third-party-licenses.darwin.md
@@ -9,10 +9,10 @@ Some packages may only be included on certain architectures or operating systems
 
 - [al.essio.dev/pkg/shellescape](https://pkg.go.dev/al.essio.dev/pkg/shellescape) ([MIT](https://github.com/alessio/shellescape/blob/v1.6.0/LICENSE))
 - [cel.dev/expr](https://pkg.go.dev/cel.dev/expr) ([Apache-2.0](https://github.com/google/cel-spec/blob/v0.24.0/LICENSE))
-- [cloud.google.com/go](https://pkg.go.dev/cloud.google.com/go) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/v0.121.1/LICENSE))
-- [cloud.google.com/go/auth](https://pkg.go.dev/cloud.google.com/go/auth) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/v0.16.2/auth/LICENSE))
+- [cloud.google.com/go](https://pkg.go.dev/cloud.google.com/go) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/v0.121.4/LICENSE))
+- [cloud.google.com/go/auth](https://pkg.go.dev/cloud.google.com/go/auth) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/v0.16.5/auth/LICENSE))
 - [cloud.google.com/go/auth/oauth2adapt](https://pkg.go.dev/cloud.google.com/go/auth/oauth2adapt) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/oauth2adapt/v0.2.8/auth/oauth2adapt/LICENSE))
-- [cloud.google.com/go/compute/metadata](https://pkg.go.dev/cloud.google.com/go/compute/metadata) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/compute/metadata/v0.7.0/compute/metadata/LICENSE))
+- [cloud.google.com/go/compute/metadata](https://pkg.go.dev/cloud.google.com/go/compute/metadata) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/compute/metadata/v0.8.0/compute/metadata/LICENSE))
 - [cloud.google.com/go/iam](https://pkg.go.dev/cloud.google.com/go/iam) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/iam/v1.5.2/iam/LICENSE))
 - [cloud.google.com/go/longrunning](https://pkg.go.dev/cloud.google.com/go/longrunning) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/longrunning/v0.6.7/longrunning/LICENSE))
 - [cloud.google.com/go/monitoring](https://pkg.go.dev/cloud.google.com/go/monitoring) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/monitoring/v1.24.2/monitoring/LICENSE))
@@ -22,7 +22,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/AlecAivazis/survey/v2](https://pkg.go.dev/github.com/AlecAivazis/survey/v2) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/LICENSE))
 - [github.com/AlecAivazis/survey/v2/terminal](https://pkg.go.dev/github.com/AlecAivazis/survey/v2/terminal) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/terminal/LICENSE.txt))
 - [github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/grpc-gcp-go/blob/grpcgcp/v1.5.3/grpcgcp/LICENSE))
-- [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/detectors/gcp/v1.27.0/detectors/gcp/LICENSE))
+- [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/detectors/gcp/v1.29.0/detectors/gcp/LICENSE))
 - [github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/exporter/metric/v0.53.0/exporter/metric/LICENSE))
 - [github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/internal/resourcemapping/v0.53.0/internal/resourcemapping/LICENSE))
 - [github.com/MakeNowJust/heredoc](https://pkg.go.dev/github.com/MakeNowJust/heredoc) ([MIT](https://github.com/MakeNowJust/heredoc/blob/v1.0.0/LICENSE))
@@ -79,13 +79,13 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.1/LICENSE))
 - [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.8.1/LICENSE))
 - [github.com/globocom/go-buffer](https://pkg.go.dev/github.com/globocom/go-buffer) ([MIT](https://github.com/globocom/go-buffer/blob/v1.2.2/LICENSE))
-- [github.com/go-chi/chi](https://pkg.go.dev/github.com/go-chi/chi) ([MIT](https://github.com/go-chi/chi/blob/v4.1.2/LICENSE))
+- [github.com/go-chi/chi/v5](https://pkg.go.dev/github.com/go-chi/chi/v5) ([MIT](https://github.com/go-chi/chi/blob/v5.2.2/LICENSE))
 - [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.1.1/LICENSE))
 - [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.1.1/json/LICENSE))
 - [github.com/go-logr/logr](https://pkg.go.dev/github.com/go-logr/logr) ([Apache-2.0](https://github.com/go-logr/logr/blob/v1.4.3/LICENSE))
 - [github.com/go-logr/stdr](https://pkg.go.dev/github.com/go-logr/stdr) ([Apache-2.0](https://github.com/go-logr/stdr/blob/v1.2.2/LICENSE))
 - [github.com/go-openapi/analysis](https://pkg.go.dev/github.com/go-openapi/analysis) ([Apache-2.0](https://github.com/go-openapi/analysis/blob/v0.23.0/LICENSE))
-- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.1/LICENSE))
+- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.2/LICENSE))
 - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.1/LICENSE))
 - [github.com/go-openapi/jsonreference](https://pkg.go.dev/github.com/go-openapi/jsonreference) ([Apache-2.0](https://github.com/go-openapi/jsonreference/blob/v0.21.0/LICENSE))
 - [github.com/go-openapi/loads](https://pkg.go.dev/github.com/go-openapi/loads) ([Apache-2.0](https://github.com/go-openapi/loads/blob/v0.22.0/LICENSE))
@@ -95,7 +95,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/go-openapi/strfmt](https://pkg.go.dev/github.com/go-openapi/strfmt) ([Apache-2.0](https://github.com/go-openapi/strfmt/blob/v0.23.0/LICENSE))
 - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.23.1/LICENSE))
 - [github.com/go-openapi/validate](https://pkg.go.dev/github.com/go-openapi/validate) ([Apache-2.0](https://github.com/go-openapi/validate/blob/v0.24.0/LICENSE))
-- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE))
+- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE))
 - [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/2c02b8208cf8/LICENSE))
 - [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v1.0.0/LICENSE))
 - [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.2/LICENSE))
@@ -105,7 +105,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/google/shlex](https://pkg.go.dev/github.com/google/shlex) ([Apache-2.0](https://github.com/google/shlex/blob/e7afc7fbc510/COPYING))
 - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
 - [github.com/googleapis/enterprise-certificate-proxy/client](https://pkg.go.dev/github.com/googleapis/enterprise-certificate-proxy/client) ([Apache-2.0](https://github.com/googleapis/enterprise-certificate-proxy/blob/v0.3.6/LICENSE))
-- [github.com/googleapis/gax-go/v2](https://pkg.go.dev/github.com/googleapis/gax-go/v2) ([BSD-3-Clause](https://github.com/googleapis/gax-go/blob/v2.14.2/v2/LICENSE))
+- [github.com/googleapis/gax-go/v2](https://pkg.go.dev/github.com/googleapis/gax-go/v2) ([BSD-3-Clause](https://github.com/googleapis/gax-go/blob/v2.15.0/v2/LICENSE))
 - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
 - [github.com/gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) ([BSD-2-Clause](https://github.com/gorilla/websocket/blob/v1.5.3/LICENSE))
 - [github.com/grpc-ecosystem/grpc-gateway/v2](https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/v2) ([BSD-3-Clause](https://github.com/grpc-ecosystem/grpc-gateway/blob/v2.27.0/LICENSE))
@@ -159,14 +159,14 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/russross/blackfriday/v2](https://pkg.go.dev/github.com/russross/blackfriday/v2) ([BSD-2-Clause](https://github.com/russross/blackfriday/blob/v2.1.0/LICENSE.txt))
 - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
 - [github.com/sassoftware/relic/lib](https://pkg.go.dev/github.com/sassoftware/relic/lib) ([Apache-2.0](https://github.com/sassoftware/relic/blob/v7.2.1/LICENSE))
-- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.0/LICENSE))
+- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.1/LICENSE))
 - [github.com/shibumi/go-pathspec](https://pkg.go.dev/github.com/shibumi/go-pathspec) ([Apache-2.0](https://github.com/shibumi/go-pathspec/blob/v1.3.0/LICENSE))
 - [github.com/shopspring/decimal](https://pkg.go.dev/github.com/shopspring/decimal) ([MIT](https://github.com/shopspring/decimal/blob/v1.4.0/LICENSE))
 - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
 - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
 - [github.com/sigstore/protobuf-specs/gen/pb-go](https://pkg.go.dev/github.com/sigstore/protobuf-specs/gen/pb-go) ([Apache-2.0](https://github.com/sigstore/protobuf-specs/blob/v0.5.0/LICENSE))
 - [github.com/sigstore/rekor-tiles](https://pkg.go.dev/github.com/sigstore/rekor-tiles) ([Apache-2.0](https://github.com/sigstore/rekor-tiles/blob/98cd4a77300f/LICENSE))
-- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.3.10/LICENSE))
+- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.4.1/LICENSE))
 - [github.com/sigstore/sigstore-go/pkg](https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore-go/blob/v1.1.0/LICENSE))
 - [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.5/LICENSE))
 - [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.8/LICENSE))
@@ -179,7 +179,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE))
 - [github.com/spiffe/go-spiffe/v2](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2) ([Apache-2.0](https://github.com/spiffe/go-spiffe/blob/v2.5.0/v2/LICENSE))
 - [github.com/stretchr/objx](https://pkg.go.dev/github.com/stretchr/objx) ([MIT](https://github.com/stretchr/objx/blob/v0.5.2/LICENSE))
-- [github.com/stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify) ([MIT](https://github.com/stretchr/testify/blob/v1.10.0/LICENSE))
+- [github.com/stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify) ([MIT](https://github.com/stretchr/testify/blob/v1.11.0/LICENSE))
 - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
 - [github.com/theupdateframework/go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf) ([BSD-3-Clause](https://github.com/theupdateframework/go-tuf/blob/v0.7.0/LICENSE))
 - [github.com/theupdateframework/go-tuf/v2/metadata](https://pkg.go.dev/github.com/theupdateframework/go-tuf/v2/metadata) ([Apache-2.0](https://github.com/theupdateframework/go-tuf/blob/v2.1.1/LICENSE))
@@ -189,6 +189,8 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/transparency-dev/merkle](https://pkg.go.dev/github.com/transparency-dev/merkle) ([Apache-2.0](https://github.com/transparency-dev/merkle/blob/v0.0.2/LICENSE))
 - [github.com/transparency-dev/tessera](https://pkg.go.dev/github.com/transparency-dev/tessera) ([Apache-2.0](https://github.com/transparency-dev/tessera/blob/8ee4e93b2823/LICENSE))
 - [github.com/vbatts/tar-split/archive/tar](https://pkg.go.dev/github.com/vbatts/tar-split/archive/tar) ([BSD-3-Clause](https://github.com/vbatts/tar-split/blob/v0.12.1/LICENSE))
+- [github.com/vmihailenco/msgpack/v5](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5) ([BSD-2-Clause](https://github.com/vmihailenco/msgpack/blob/v5.4.1/LICENSE))
+- [github.com/vmihailenco/tagparser/v2](https://pkg.go.dev/github.com/vmihailenco/tagparser/v2) ([BSD-2-Clause](https://github.com/vmihailenco/tagparser/blob/v2.0.0/LICENSE))
 - [github.com/xo/terminfo](https://pkg.go.dev/github.com/xo/terminfo) ([MIT](https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE))
 - [github.com/yuin/goldmark](https://pkg.go.dev/github.com/yuin/goldmark) ([MIT](https://github.com/yuin/goldmark/blob/v1.7.13/LICENSE))
 - [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.6/LICENSE))
@@ -202,14 +204,14 @@ Some packages may only be included on certain architectures or operating systems
 - [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/instrumentation/net/http/otelhttp/v0.61.0/instrumentation/net/http/otelhttp/LICENSE))
 - [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.37.0/LICENSE))
 - [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.37.0/metric/LICENSE))
-- [go.opentelemetry.io/otel/sdk](https://pkg.go.dev/go.opentelemetry.io/otel/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/v1.36.0/sdk/LICENSE))
-- [go.opentelemetry.io/otel/sdk/metric](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/metric/v1.36.0/sdk/metric/LICENSE))
+- [go.opentelemetry.io/otel/sdk](https://pkg.go.dev/go.opentelemetry.io/otel/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/v1.37.0/sdk/LICENSE))
+- [go.opentelemetry.io/otel/sdk/metric](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/metric/v1.37.0/sdk/metric/LICENSE))
 - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.37.0/trace/LICENSE))
 - [go.uber.org/multierr](https://pkg.go.dev/go.uber.org/multierr) ([MIT](https://github.com/uber-go/multierr/blob/v1.11.0/LICENSE.txt))
 - [go.uber.org/zap](https://pkg.go.dev/go.uber.org/zap) ([MIT](https://github.com/uber-go/zap/blob/v1.27.0/LICENSE))
 - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.41.0:LICENSE))
 - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b7579e27:LICENSE))
-- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.26.0:LICENSE))
+- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.27.0:LICENSE))
 - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.43.0:LICENSE))
 - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE))
 - [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.16.0:LICENSE))
@@ -217,13 +219,13 @@ Some packages may only be included on certain architectures or operating systems
 - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.34.0:LICENSE))
 - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.28.0:LICENSE))
 - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.12.0:LICENSE))
-- [google.golang.org/api](https://pkg.go.dev/google.golang.org/api) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.238.0/LICENSE))
-- [google.golang.org/api/internal/third_party/uritemplates](https://pkg.go.dev/google.golang.org/api/internal/third_party/uritemplates) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.238.0/internal/third_party/uritemplates/LICENSE))
-- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/api/LICENSE))
-- [google.golang.org/genproto/googleapis/rpc](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/5f3141c8851a/googleapis/rpc/LICENSE))
-- [google.golang.org/genproto/googleapis/type](https://pkg.go.dev/google.golang.org/genproto/googleapis/type) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/f936aa4a68b2/LICENSE))
-- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.74.2/LICENSE))
-- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.7/LICENSE))
+- [google.golang.org/api](https://pkg.go.dev/google.golang.org/api) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.248.0/LICENSE))
+- [google.golang.org/api/internal/third_party/uritemplates](https://pkg.go.dev/google.golang.org/api/internal/third_party/uritemplates) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.248.0/internal/third_party/uritemplates/LICENSE))
+- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/a45f3dfb1074/googleapis/api/LICENSE))
+- [google.golang.org/genproto/googleapis/rpc](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/3122310a409c/googleapis/rpc/LICENSE))
+- [google.golang.org/genproto/googleapis/type](https://pkg.go.dev/google.golang.org/genproto/googleapis/type) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/LICENSE))
+- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.75.0/LICENSE))
+- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.8/LICENSE))
 - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
 - [k8s.io/klog/v2](https://pkg.go.dev/k8s.io/klog/v2) ([Apache-2.0](https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE))
 
diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md
index 3281ddfe1..15f27fb64 100644
--- a/third-party-licenses.linux.md
+++ b/third-party-licenses.linux.md
@@ -8,10 +8,10 @@ Some packages may only be included on certain architectures or operating systems
 
 
 - [cel.dev/expr](https://pkg.go.dev/cel.dev/expr) ([Apache-2.0](https://github.com/google/cel-spec/blob/v0.24.0/LICENSE))
-- [cloud.google.com/go](https://pkg.go.dev/cloud.google.com/go) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/v0.121.1/LICENSE))
-- [cloud.google.com/go/auth](https://pkg.go.dev/cloud.google.com/go/auth) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/v0.16.2/auth/LICENSE))
+- [cloud.google.com/go](https://pkg.go.dev/cloud.google.com/go) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/v0.121.4/LICENSE))
+- [cloud.google.com/go/auth](https://pkg.go.dev/cloud.google.com/go/auth) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/v0.16.5/auth/LICENSE))
 - [cloud.google.com/go/auth/oauth2adapt](https://pkg.go.dev/cloud.google.com/go/auth/oauth2adapt) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/oauth2adapt/v0.2.8/auth/oauth2adapt/LICENSE))
-- [cloud.google.com/go/compute/metadata](https://pkg.go.dev/cloud.google.com/go/compute/metadata) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/compute/metadata/v0.7.0/compute/metadata/LICENSE))
+- [cloud.google.com/go/compute/metadata](https://pkg.go.dev/cloud.google.com/go/compute/metadata) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/compute/metadata/v0.8.0/compute/metadata/LICENSE))
 - [cloud.google.com/go/iam](https://pkg.go.dev/cloud.google.com/go/iam) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/iam/v1.5.2/iam/LICENSE))
 - [cloud.google.com/go/longrunning](https://pkg.go.dev/cloud.google.com/go/longrunning) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/longrunning/v0.6.7/longrunning/LICENSE))
 - [cloud.google.com/go/monitoring](https://pkg.go.dev/cloud.google.com/go/monitoring) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/monitoring/v1.24.2/monitoring/LICENSE))
@@ -21,7 +21,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/AlecAivazis/survey/v2](https://pkg.go.dev/github.com/AlecAivazis/survey/v2) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/LICENSE))
 - [github.com/AlecAivazis/survey/v2/terminal](https://pkg.go.dev/github.com/AlecAivazis/survey/v2/terminal) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/terminal/LICENSE.txt))
 - [github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/grpc-gcp-go/blob/grpcgcp/v1.5.3/grpcgcp/LICENSE))
-- [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/detectors/gcp/v1.27.0/detectors/gcp/LICENSE))
+- [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/detectors/gcp/v1.29.0/detectors/gcp/LICENSE))
 - [github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/exporter/metric/v0.53.0/exporter/metric/LICENSE))
 - [github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/internal/resourcemapping/v0.53.0/internal/resourcemapping/LICENSE))
 - [github.com/MakeNowJust/heredoc](https://pkg.go.dev/github.com/MakeNowJust/heredoc) ([MIT](https://github.com/MakeNowJust/heredoc/blob/v1.0.0/LICENSE))
@@ -78,13 +78,13 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.1/LICENSE))
 - [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.8.1/LICENSE))
 - [github.com/globocom/go-buffer](https://pkg.go.dev/github.com/globocom/go-buffer) ([MIT](https://github.com/globocom/go-buffer/blob/v1.2.2/LICENSE))
-- [github.com/go-chi/chi](https://pkg.go.dev/github.com/go-chi/chi) ([MIT](https://github.com/go-chi/chi/blob/v4.1.2/LICENSE))
+- [github.com/go-chi/chi/v5](https://pkg.go.dev/github.com/go-chi/chi/v5) ([MIT](https://github.com/go-chi/chi/blob/v5.2.2/LICENSE))
 - [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.1.1/LICENSE))
 - [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.1.1/json/LICENSE))
 - [github.com/go-logr/logr](https://pkg.go.dev/github.com/go-logr/logr) ([Apache-2.0](https://github.com/go-logr/logr/blob/v1.4.3/LICENSE))
 - [github.com/go-logr/stdr](https://pkg.go.dev/github.com/go-logr/stdr) ([Apache-2.0](https://github.com/go-logr/stdr/blob/v1.2.2/LICENSE))
 - [github.com/go-openapi/analysis](https://pkg.go.dev/github.com/go-openapi/analysis) ([Apache-2.0](https://github.com/go-openapi/analysis/blob/v0.23.0/LICENSE))
-- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.1/LICENSE))
+- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.2/LICENSE))
 - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.1/LICENSE))
 - [github.com/go-openapi/jsonreference](https://pkg.go.dev/github.com/go-openapi/jsonreference) ([Apache-2.0](https://github.com/go-openapi/jsonreference/blob/v0.21.0/LICENSE))
 - [github.com/go-openapi/loads](https://pkg.go.dev/github.com/go-openapi/loads) ([Apache-2.0](https://github.com/go-openapi/loads/blob/v0.22.0/LICENSE))
@@ -94,7 +94,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/go-openapi/strfmt](https://pkg.go.dev/github.com/go-openapi/strfmt) ([Apache-2.0](https://github.com/go-openapi/strfmt/blob/v0.23.0/LICENSE))
 - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.23.1/LICENSE))
 - [github.com/go-openapi/validate](https://pkg.go.dev/github.com/go-openapi/validate) ([Apache-2.0](https://github.com/go-openapi/validate/blob/v0.24.0/LICENSE))
-- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE))
+- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE))
 - [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE))
 - [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/2c02b8208cf8/LICENSE))
 - [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v1.0.0/LICENSE))
@@ -105,7 +105,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/google/shlex](https://pkg.go.dev/github.com/google/shlex) ([Apache-2.0](https://github.com/google/shlex/blob/e7afc7fbc510/COPYING))
 - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
 - [github.com/googleapis/enterprise-certificate-proxy/client](https://pkg.go.dev/github.com/googleapis/enterprise-certificate-proxy/client) ([Apache-2.0](https://github.com/googleapis/enterprise-certificate-proxy/blob/v0.3.6/LICENSE))
-- [github.com/googleapis/gax-go/v2](https://pkg.go.dev/github.com/googleapis/gax-go/v2) ([BSD-3-Clause](https://github.com/googleapis/gax-go/blob/v2.14.2/v2/LICENSE))
+- [github.com/googleapis/gax-go/v2](https://pkg.go.dev/github.com/googleapis/gax-go/v2) ([BSD-3-Clause](https://github.com/googleapis/gax-go/blob/v2.15.0/v2/LICENSE))
 - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
 - [github.com/gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) ([BSD-2-Clause](https://github.com/gorilla/websocket/blob/v1.5.3/LICENSE))
 - [github.com/grpc-ecosystem/grpc-gateway/v2](https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/v2) ([BSD-3-Clause](https://github.com/grpc-ecosystem/grpc-gateway/blob/v2.27.0/LICENSE))
@@ -159,14 +159,14 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/russross/blackfriday/v2](https://pkg.go.dev/github.com/russross/blackfriday/v2) ([BSD-2-Clause](https://github.com/russross/blackfriday/blob/v2.1.0/LICENSE.txt))
 - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
 - [github.com/sassoftware/relic/lib](https://pkg.go.dev/github.com/sassoftware/relic/lib) ([Apache-2.0](https://github.com/sassoftware/relic/blob/v7.2.1/LICENSE))
-- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.0/LICENSE))
+- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.1/LICENSE))
 - [github.com/shibumi/go-pathspec](https://pkg.go.dev/github.com/shibumi/go-pathspec) ([Apache-2.0](https://github.com/shibumi/go-pathspec/blob/v1.3.0/LICENSE))
 - [github.com/shopspring/decimal](https://pkg.go.dev/github.com/shopspring/decimal) ([MIT](https://github.com/shopspring/decimal/blob/v1.4.0/LICENSE))
 - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
 - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
 - [github.com/sigstore/protobuf-specs/gen/pb-go](https://pkg.go.dev/github.com/sigstore/protobuf-specs/gen/pb-go) ([Apache-2.0](https://github.com/sigstore/protobuf-specs/blob/v0.5.0/LICENSE))
 - [github.com/sigstore/rekor-tiles](https://pkg.go.dev/github.com/sigstore/rekor-tiles) ([Apache-2.0](https://github.com/sigstore/rekor-tiles/blob/98cd4a77300f/LICENSE))
-- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.3.10/LICENSE))
+- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.4.1/LICENSE))
 - [github.com/sigstore/sigstore-go/pkg](https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore-go/blob/v1.1.0/LICENSE))
 - [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.5/LICENSE))
 - [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.8/LICENSE))
@@ -179,7 +179,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE))
 - [github.com/spiffe/go-spiffe/v2](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2) ([Apache-2.0](https://github.com/spiffe/go-spiffe/blob/v2.5.0/v2/LICENSE))
 - [github.com/stretchr/objx](https://pkg.go.dev/github.com/stretchr/objx) ([MIT](https://github.com/stretchr/objx/blob/v0.5.2/LICENSE))
-- [github.com/stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify) ([MIT](https://github.com/stretchr/testify/blob/v1.10.0/LICENSE))
+- [github.com/stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify) ([MIT](https://github.com/stretchr/testify/blob/v1.11.0/LICENSE))
 - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
 - [github.com/theupdateframework/go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf) ([BSD-3-Clause](https://github.com/theupdateframework/go-tuf/blob/v0.7.0/LICENSE))
 - [github.com/theupdateframework/go-tuf/v2/metadata](https://pkg.go.dev/github.com/theupdateframework/go-tuf/v2/metadata) ([Apache-2.0](https://github.com/theupdateframework/go-tuf/blob/v2.1.1/LICENSE))
@@ -189,6 +189,8 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/transparency-dev/merkle](https://pkg.go.dev/github.com/transparency-dev/merkle) ([Apache-2.0](https://github.com/transparency-dev/merkle/blob/v0.0.2/LICENSE))
 - [github.com/transparency-dev/tessera](https://pkg.go.dev/github.com/transparency-dev/tessera) ([Apache-2.0](https://github.com/transparency-dev/tessera/blob/8ee4e93b2823/LICENSE))
 - [github.com/vbatts/tar-split/archive/tar](https://pkg.go.dev/github.com/vbatts/tar-split/archive/tar) ([BSD-3-Clause](https://github.com/vbatts/tar-split/blob/v0.12.1/LICENSE))
+- [github.com/vmihailenco/msgpack/v5](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5) ([BSD-2-Clause](https://github.com/vmihailenco/msgpack/blob/v5.4.1/LICENSE))
+- [github.com/vmihailenco/tagparser/v2](https://pkg.go.dev/github.com/vmihailenco/tagparser/v2) ([BSD-2-Clause](https://github.com/vmihailenco/tagparser/blob/v2.0.0/LICENSE))
 - [github.com/xo/terminfo](https://pkg.go.dev/github.com/xo/terminfo) ([MIT](https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE))
 - [github.com/yuin/goldmark](https://pkg.go.dev/github.com/yuin/goldmark) ([MIT](https://github.com/yuin/goldmark/blob/v1.7.13/LICENSE))
 - [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.6/LICENSE))
@@ -202,14 +204,14 @@ Some packages may only be included on certain architectures or operating systems
 - [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/instrumentation/net/http/otelhttp/v0.61.0/instrumentation/net/http/otelhttp/LICENSE))
 - [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.37.0/LICENSE))
 - [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.37.0/metric/LICENSE))
-- [go.opentelemetry.io/otel/sdk](https://pkg.go.dev/go.opentelemetry.io/otel/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/v1.36.0/sdk/LICENSE))
-- [go.opentelemetry.io/otel/sdk/metric](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/metric/v1.36.0/sdk/metric/LICENSE))
+- [go.opentelemetry.io/otel/sdk](https://pkg.go.dev/go.opentelemetry.io/otel/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/v1.37.0/sdk/LICENSE))
+- [go.opentelemetry.io/otel/sdk/metric](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/metric/v1.37.0/sdk/metric/LICENSE))
 - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.37.0/trace/LICENSE))
 - [go.uber.org/multierr](https://pkg.go.dev/go.uber.org/multierr) ([MIT](https://github.com/uber-go/multierr/blob/v1.11.0/LICENSE.txt))
 - [go.uber.org/zap](https://pkg.go.dev/go.uber.org/zap) ([MIT](https://github.com/uber-go/zap/blob/v1.27.0/LICENSE))
 - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.41.0:LICENSE))
 - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b7579e27:LICENSE))
-- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.26.0:LICENSE))
+- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.27.0:LICENSE))
 - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.43.0:LICENSE))
 - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE))
 - [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.16.0:LICENSE))
@@ -217,13 +219,13 @@ Some packages may only be included on certain architectures or operating systems
 - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.34.0:LICENSE))
 - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.28.0:LICENSE))
 - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.12.0:LICENSE))
-- [google.golang.org/api](https://pkg.go.dev/google.golang.org/api) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.238.0/LICENSE))
-- [google.golang.org/api/internal/third_party/uritemplates](https://pkg.go.dev/google.golang.org/api/internal/third_party/uritemplates) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.238.0/internal/third_party/uritemplates/LICENSE))
-- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/api/LICENSE))
-- [google.golang.org/genproto/googleapis/rpc](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/5f3141c8851a/googleapis/rpc/LICENSE))
-- [google.golang.org/genproto/googleapis/type](https://pkg.go.dev/google.golang.org/genproto/googleapis/type) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/f936aa4a68b2/LICENSE))
-- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.74.2/LICENSE))
-- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.7/LICENSE))
+- [google.golang.org/api](https://pkg.go.dev/google.golang.org/api) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.248.0/LICENSE))
+- [google.golang.org/api/internal/third_party/uritemplates](https://pkg.go.dev/google.golang.org/api/internal/third_party/uritemplates) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.248.0/internal/third_party/uritemplates/LICENSE))
+- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/a45f3dfb1074/googleapis/api/LICENSE))
+- [google.golang.org/genproto/googleapis/rpc](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/3122310a409c/googleapis/rpc/LICENSE))
+- [google.golang.org/genproto/googleapis/type](https://pkg.go.dev/google.golang.org/genproto/googleapis/type) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/LICENSE))
+- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.75.0/LICENSE))
+- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.8/LICENSE))
 - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
 - [k8s.io/klog/v2](https://pkg.go.dev/k8s.io/klog/v2) ([Apache-2.0](https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE))
 
diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md
index 52e6d759b..402c9025b 100644
--- a/third-party-licenses.windows.md
+++ b/third-party-licenses.windows.md
@@ -8,10 +8,10 @@ Some packages may only be included on certain architectures or operating systems
 
 
 - [cel.dev/expr](https://pkg.go.dev/cel.dev/expr) ([Apache-2.0](https://github.com/google/cel-spec/blob/v0.24.0/LICENSE))
-- [cloud.google.com/go](https://pkg.go.dev/cloud.google.com/go) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/v0.121.1/LICENSE))
-- [cloud.google.com/go/auth](https://pkg.go.dev/cloud.google.com/go/auth) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/v0.16.2/auth/LICENSE))
+- [cloud.google.com/go](https://pkg.go.dev/cloud.google.com/go) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/v0.121.4/LICENSE))
+- [cloud.google.com/go/auth](https://pkg.go.dev/cloud.google.com/go/auth) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/v0.16.5/auth/LICENSE))
 - [cloud.google.com/go/auth/oauth2adapt](https://pkg.go.dev/cloud.google.com/go/auth/oauth2adapt) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/auth/oauth2adapt/v0.2.8/auth/oauth2adapt/LICENSE))
-- [cloud.google.com/go/compute/metadata](https://pkg.go.dev/cloud.google.com/go/compute/metadata) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/compute/metadata/v0.7.0/compute/metadata/LICENSE))
+- [cloud.google.com/go/compute/metadata](https://pkg.go.dev/cloud.google.com/go/compute/metadata) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/compute/metadata/v0.8.0/compute/metadata/LICENSE))
 - [cloud.google.com/go/iam](https://pkg.go.dev/cloud.google.com/go/iam) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/iam/v1.5.2/iam/LICENSE))
 - [cloud.google.com/go/longrunning](https://pkg.go.dev/cloud.google.com/go/longrunning) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/longrunning/v0.6.7/longrunning/LICENSE))
 - [cloud.google.com/go/monitoring](https://pkg.go.dev/cloud.google.com/go/monitoring) ([Apache-2.0](https://github.com/googleapis/google-cloud-go/blob/monitoring/v1.24.2/monitoring/LICENSE))
@@ -21,7 +21,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/AlecAivazis/survey/v2](https://pkg.go.dev/github.com/AlecAivazis/survey/v2) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/LICENSE))
 - [github.com/AlecAivazis/survey/v2/terminal](https://pkg.go.dev/github.com/AlecAivazis/survey/v2/terminal) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/terminal/LICENSE.txt))
 - [github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/grpc-gcp-go/blob/grpcgcp/v1.5.3/grpcgcp/LICENSE))
-- [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/detectors/gcp/v1.27.0/detectors/gcp/LICENSE))
+- [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/detectors/gcp/v1.29.0/detectors/gcp/LICENSE))
 - [github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/exporter/metric/v0.53.0/exporter/metric/LICENSE))
 - [github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping](https://pkg.go.dev/github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping) ([Apache-2.0](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/internal/resourcemapping/v0.53.0/internal/resourcemapping/LICENSE))
 - [github.com/MakeNowJust/heredoc](https://pkg.go.dev/github.com/MakeNowJust/heredoc) ([MIT](https://github.com/MakeNowJust/heredoc/blob/v1.0.0/LICENSE))
@@ -80,13 +80,13 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.1/LICENSE))
 - [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.8.1/LICENSE))
 - [github.com/globocom/go-buffer](https://pkg.go.dev/github.com/globocom/go-buffer) ([MIT](https://github.com/globocom/go-buffer/blob/v1.2.2/LICENSE))
-- [github.com/go-chi/chi](https://pkg.go.dev/github.com/go-chi/chi) ([MIT](https://github.com/go-chi/chi/blob/v4.1.2/LICENSE))
+- [github.com/go-chi/chi/v5](https://pkg.go.dev/github.com/go-chi/chi/v5) ([MIT](https://github.com/go-chi/chi/blob/v5.2.2/LICENSE))
 - [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.1.1/LICENSE))
 - [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.1.1/json/LICENSE))
 - [github.com/go-logr/logr](https://pkg.go.dev/github.com/go-logr/logr) ([Apache-2.0](https://github.com/go-logr/logr/blob/v1.4.3/LICENSE))
 - [github.com/go-logr/stdr](https://pkg.go.dev/github.com/go-logr/stdr) ([Apache-2.0](https://github.com/go-logr/stdr/blob/v1.2.2/LICENSE))
 - [github.com/go-openapi/analysis](https://pkg.go.dev/github.com/go-openapi/analysis) ([Apache-2.0](https://github.com/go-openapi/analysis/blob/v0.23.0/LICENSE))
-- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.1/LICENSE))
+- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.2/LICENSE))
 - [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.1/LICENSE))
 - [github.com/go-openapi/jsonreference](https://pkg.go.dev/github.com/go-openapi/jsonreference) ([Apache-2.0](https://github.com/go-openapi/jsonreference/blob/v0.21.0/LICENSE))
 - [github.com/go-openapi/loads](https://pkg.go.dev/github.com/go-openapi/loads) ([Apache-2.0](https://github.com/go-openapi/loads/blob/v0.22.0/LICENSE))
@@ -96,7 +96,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/go-openapi/strfmt](https://pkg.go.dev/github.com/go-openapi/strfmt) ([Apache-2.0](https://github.com/go-openapi/strfmt/blob/v0.23.0/LICENSE))
 - [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.23.1/LICENSE))
 - [github.com/go-openapi/validate](https://pkg.go.dev/github.com/go-openapi/validate) ([Apache-2.0](https://github.com/go-openapi/validate/blob/v0.24.0/LICENSE))
-- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE))
+- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.4.0/LICENSE))
 - [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/2c02b8208cf8/LICENSE))
 - [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v1.0.0/LICENSE))
 - [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.2/LICENSE))
@@ -106,7 +106,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/google/shlex](https://pkg.go.dev/github.com/google/shlex) ([Apache-2.0](https://github.com/google/shlex/blob/e7afc7fbc510/COPYING))
 - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
 - [github.com/googleapis/enterprise-certificate-proxy/client](https://pkg.go.dev/github.com/googleapis/enterprise-certificate-proxy/client) ([Apache-2.0](https://github.com/googleapis/enterprise-certificate-proxy/blob/v0.3.6/LICENSE))
-- [github.com/googleapis/gax-go/v2](https://pkg.go.dev/github.com/googleapis/gax-go/v2) ([BSD-3-Clause](https://github.com/googleapis/gax-go/blob/v2.14.2/v2/LICENSE))
+- [github.com/googleapis/gax-go/v2](https://pkg.go.dev/github.com/googleapis/gax-go/v2) ([BSD-3-Clause](https://github.com/googleapis/gax-go/blob/v2.15.0/v2/LICENSE))
 - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
 - [github.com/gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) ([BSD-2-Clause](https://github.com/gorilla/websocket/blob/v1.5.3/LICENSE))
 - [github.com/grpc-ecosystem/grpc-gateway/v2](https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/v2) ([BSD-3-Clause](https://github.com/grpc-ecosystem/grpc-gateway/blob/v2.27.0/LICENSE))
@@ -162,14 +162,14 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/russross/blackfriday/v2](https://pkg.go.dev/github.com/russross/blackfriday/v2) ([BSD-2-Clause](https://github.com/russross/blackfriday/blob/v2.1.0/LICENSE.txt))
 - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
 - [github.com/sassoftware/relic/lib](https://pkg.go.dev/github.com/sassoftware/relic/lib) ([Apache-2.0](https://github.com/sassoftware/relic/blob/v7.2.1/LICENSE))
-- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.0/LICENSE))
+- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.1/LICENSE))
 - [github.com/shibumi/go-pathspec](https://pkg.go.dev/github.com/shibumi/go-pathspec) ([Apache-2.0](https://github.com/shibumi/go-pathspec/blob/v1.3.0/LICENSE))
 - [github.com/shopspring/decimal](https://pkg.go.dev/github.com/shopspring/decimal) ([MIT](https://github.com/shopspring/decimal/blob/v1.4.0/LICENSE))
 - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
 - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
 - [github.com/sigstore/protobuf-specs/gen/pb-go](https://pkg.go.dev/github.com/sigstore/protobuf-specs/gen/pb-go) ([Apache-2.0](https://github.com/sigstore/protobuf-specs/blob/v0.5.0/LICENSE))
 - [github.com/sigstore/rekor-tiles](https://pkg.go.dev/github.com/sigstore/rekor-tiles) ([Apache-2.0](https://github.com/sigstore/rekor-tiles/blob/98cd4a77300f/LICENSE))
-- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.3.10/LICENSE))
+- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.4.1/LICENSE))
 - [github.com/sigstore/sigstore-go/pkg](https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore-go/blob/v1.1.0/LICENSE))
 - [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.5/LICENSE))
 - [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.8/LICENSE))
@@ -182,7 +182,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE))
 - [github.com/spiffe/go-spiffe/v2](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2) ([Apache-2.0](https://github.com/spiffe/go-spiffe/blob/v2.5.0/v2/LICENSE))
 - [github.com/stretchr/objx](https://pkg.go.dev/github.com/stretchr/objx) ([MIT](https://github.com/stretchr/objx/blob/v0.5.2/LICENSE))
-- [github.com/stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify) ([MIT](https://github.com/stretchr/testify/blob/v1.10.0/LICENSE))
+- [github.com/stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify) ([MIT](https://github.com/stretchr/testify/blob/v1.11.0/LICENSE))
 - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
 - [github.com/theupdateframework/go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf) ([BSD-3-Clause](https://github.com/theupdateframework/go-tuf/blob/v0.7.0/LICENSE))
 - [github.com/theupdateframework/go-tuf/v2/metadata](https://pkg.go.dev/github.com/theupdateframework/go-tuf/v2/metadata) ([Apache-2.0](https://github.com/theupdateframework/go-tuf/blob/v2.1.1/LICENSE))
@@ -192,6 +192,8 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/transparency-dev/merkle](https://pkg.go.dev/github.com/transparency-dev/merkle) ([Apache-2.0](https://github.com/transparency-dev/merkle/blob/v0.0.2/LICENSE))
 - [github.com/transparency-dev/tessera](https://pkg.go.dev/github.com/transparency-dev/tessera) ([Apache-2.0](https://github.com/transparency-dev/tessera/blob/8ee4e93b2823/LICENSE))
 - [github.com/vbatts/tar-split/archive/tar](https://pkg.go.dev/github.com/vbatts/tar-split/archive/tar) ([BSD-3-Clause](https://github.com/vbatts/tar-split/blob/v0.12.1/LICENSE))
+- [github.com/vmihailenco/msgpack/v5](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5) ([BSD-2-Clause](https://github.com/vmihailenco/msgpack/blob/v5.4.1/LICENSE))
+- [github.com/vmihailenco/tagparser/v2](https://pkg.go.dev/github.com/vmihailenco/tagparser/v2) ([BSD-2-Clause](https://github.com/vmihailenco/tagparser/blob/v2.0.0/LICENSE))
 - [github.com/xo/terminfo](https://pkg.go.dev/github.com/xo/terminfo) ([MIT](https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE))
 - [github.com/yuin/goldmark](https://pkg.go.dev/github.com/yuin/goldmark) ([MIT](https://github.com/yuin/goldmark/blob/v1.7.13/LICENSE))
 - [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.6/LICENSE))
@@ -205,14 +207,14 @@ Some packages may only be included on certain architectures or operating systems
 - [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/instrumentation/net/http/otelhttp/v0.61.0/instrumentation/net/http/otelhttp/LICENSE))
 - [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.37.0/LICENSE))
 - [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.37.0/metric/LICENSE))
-- [go.opentelemetry.io/otel/sdk](https://pkg.go.dev/go.opentelemetry.io/otel/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/v1.36.0/sdk/LICENSE))
-- [go.opentelemetry.io/otel/sdk/metric](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/metric/v1.36.0/sdk/metric/LICENSE))
+- [go.opentelemetry.io/otel/sdk](https://pkg.go.dev/go.opentelemetry.io/otel/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/v1.37.0/sdk/LICENSE))
+- [go.opentelemetry.io/otel/sdk/metric](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/metric/v1.37.0/sdk/metric/LICENSE))
 - [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.37.0/trace/LICENSE))
 - [go.uber.org/multierr](https://pkg.go.dev/go.uber.org/multierr) ([MIT](https://github.com/uber-go/multierr/blob/v1.11.0/LICENSE.txt))
 - [go.uber.org/zap](https://pkg.go.dev/go.uber.org/zap) ([MIT](https://github.com/uber-go/zap/blob/v1.27.0/LICENSE))
 - [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.41.0:LICENSE))
 - [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b7579e27:LICENSE))
-- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.26.0:LICENSE))
+- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.27.0:LICENSE))
 - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.43.0:LICENSE))
 - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE))
 - [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.16.0:LICENSE))
@@ -220,13 +222,13 @@ Some packages may only be included on certain architectures or operating systems
 - [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.34.0:LICENSE))
 - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.28.0:LICENSE))
 - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.12.0:LICENSE))
-- [google.golang.org/api](https://pkg.go.dev/google.golang.org/api) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.238.0/LICENSE))
-- [google.golang.org/api/internal/third_party/uritemplates](https://pkg.go.dev/google.golang.org/api/internal/third_party/uritemplates) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.238.0/internal/third_party/uritemplates/LICENSE))
-- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/api/LICENSE))
-- [google.golang.org/genproto/googleapis/rpc](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/5f3141c8851a/googleapis/rpc/LICENSE))
-- [google.golang.org/genproto/googleapis/type](https://pkg.go.dev/google.golang.org/genproto/googleapis/type) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/f936aa4a68b2/LICENSE))
-- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.74.2/LICENSE))
-- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.7/LICENSE))
+- [google.golang.org/api](https://pkg.go.dev/google.golang.org/api) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.248.0/LICENSE))
+- [google.golang.org/api/internal/third_party/uritemplates](https://pkg.go.dev/google.golang.org/api/internal/third_party/uritemplates) ([BSD-3-Clause](https://github.com/googleapis/google-api-go-client/blob/v0.248.0/internal/third_party/uritemplates/LICENSE))
+- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/a45f3dfb1074/googleapis/api/LICENSE))
+- [google.golang.org/genproto/googleapis/rpc](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/3122310a409c/googleapis/rpc/LICENSE))
+- [google.golang.org/genproto/googleapis/type](https://pkg.go.dev/google.golang.org/genproto/googleapis/type) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/LICENSE))
+- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.75.0/LICENSE))
+- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.8/LICENSE))
 - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
 - [k8s.io/klog/v2](https://pkg.go.dev/k8s.io/klog/v2) ([Apache-2.0](https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE))
 
diff --git a/third-party/github.com/go-chi/chi/LICENSE b/third-party/github.com/go-chi/chi/v5/LICENSE
similarity index 100%
rename from third-party/github.com/go-chi/chi/LICENSE
rename to third-party/github.com/go-chi/chi/v5/LICENSE
diff --git a/third-party/github.com/vmihailenco/msgpack/v5/LICENSE b/third-party/github.com/vmihailenco/msgpack/v5/LICENSE
new file mode 100644
index 000000000..b749d0707
--- /dev/null
+++ b/third-party/github.com/vmihailenco/msgpack/v5/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2013 The github.com/vmihailenco/msgpack Authors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/third-party/github.com/vmihailenco/tagparser/v2/LICENSE b/third-party/github.com/vmihailenco/tagparser/v2/LICENSE
new file mode 100644
index 000000000..3fc93fdff
--- /dev/null
+++ b/third-party/github.com/vmihailenco/tagparser/v2/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2019 The github.com/vmihailenco/tagparser Authors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



Test patch:
diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go
index e850546a7..18a846268 100644
--- a/internal/featuredetection/feature_detection_test.go
+++ b/internal/featuredetection/feature_detection_test.go
@@ -439,3 +439,149 @@ func TestProjectV1Support(t *testing.T) {
 		})
 	}
 }
+
+func TestAdvancedIssueSearchSupport(t *testing.T) {
+	withIssueAdvanced := `{"data":{"SearchType":{"enumValues":[{"name":"ISSUE"},{"name":"ISSUE_ADVANCED"},{"name":"REPOSITORY"},{"name":"USER"},{"name":"DISCUSSION"}]}}}`
+	withoutIssueAdvanced := `{"data":{"SearchType":{"enumValues":[{"name":"ISSUE"},{"name":"REPOSITORY"},{"name":"USER"},{"name":"DISCUSSION"}]}}}`
+
+	tests := []struct {
+		name         string
+		hostname     string
+		httpStubs    func(*httpmock.Registry)
+		wantFeatures SearchFeatures
+	}{
+		{
+			name:     "github.com, before ISSUE_ADVANCED cleanup",
+			hostname: "github.com",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOptIn,
+		},
+		{
+			name:     "github.com, after ISSUE_ADVANCED cleanup",
+			hostname: "github.com",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withoutIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
+		},
+		{
+			name:     "ghec data residency (ghe.com), before ISSUE_ADVANCED cleanup",
+			hostname: "stampname.ghe.com",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOptIn,
+		},
+		{
+			name:     "ghec data residency (ghe.com), after ISSUE_ADVANCED cleanup",
+			hostname: "stampname.ghe.com",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withoutIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
+		},
+		{
+			name:     "GHE 3.18, before ISSUE_ADVANCED cleanup",
+			hostname: "git.my.org",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "api/v3/meta"),
+					httpmock.StringResponse(`{"installed_version":"3.18.0"}`),
+				)
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOptIn,
+		},
+		{
+			name:     "GHE 3.18, after ISSUE_ADVANCED cleanup",
+			hostname: "git.my.org",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "api/v3/meta"),
+					httpmock.StringResponse(`{"installed_version":"3.18.0"}`),
+				)
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withoutIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
+		},
+		{
+			name:     "GHE >3.18, before ISSUE_ADVANCED cleanup",
+			hostname: "git.my.org",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "api/v3/meta"),
+					httpmock.StringResponse(`{"installed_version":"3.18.1"}`),
+				)
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOptIn,
+		},
+		{
+			name:     "GHE >3.18, after ISSUE_ADVANCED cleanup",
+			hostname: "git.my.org",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "api/v3/meta"),
+					httpmock.StringResponse(`{"installed_version":"3.18.1"}`),
+				)
+				reg.Register(
+					httpmock.GraphQL(`query SearchType_enumValues\b`),
+					httpmock.StringResponse(withoutIssueAdvanced),
+				)
+			},
+			wantFeatures: advancedIssueSearchSupportedAsOnlyBackend,
+		},
+		{
+			name:     "GHE <3.18 (no advanced issue search support)",
+			hostname: "git.my.org",
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", "api/v3/meta"),
+					httpmock.StringResponse(`{"installed_version":"3.17.999"}`),
+				)
+			},
+			wantFeatures: advancedIssueSearchNotSupported,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			reg := &httpmock.Registry{}
+			if tt.httpStubs != nil {
+				tt.httpStubs(reg)
+			}
+			httpClient := &http.Client{}
+			httpmock.ReplaceTripper(httpClient, reg)
+
+			detector := NewDetector(httpClient, tt.hostname)
+
+			features, err := detector.SearchFeatures()
+			require.NoError(t, err)
+			require.Equal(t, tt.wantFeatures, features)
+		})
+	}
+}
diff --git a/pkg/cmd/agent-task/agent_task_test.go b/pkg/cmd/agent-task/agent_task_test.go
new file mode 100644
index 000000000..dd4fe21b0
--- /dev/null
+++ b/pkg/cmd/agent-task/agent_task_test.go
@@ -0,0 +1,149 @@
+package agent
+
+import (
+	"testing"
+
+	"github.com/cli/cli/v2/internal/config"
+	"github.com/cli/cli/v2/internal/gh"
+	ghmock "github.com/cli/cli/v2/internal/gh/mock"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/stretchr/testify/require"
+)
+
+// setupMockOAuthConfig configures a blank config with a default host and optional token behavior.
+func setupMockOAuthConfig(t *testing.T, tokenSource string) gh.Config {
+	t.Helper()
+	c := config.NewBlankConfig()
+	switch tokenSource {
+	case "oauth_token":
+		// valid OAuth device flow token stored in config
+		c.Set("github.com", "oauth_token", "gho_OAUTH123")
+	case "keyring":
+		// valid OAuth device flow token stored in keyring
+		c.Set("github.com", "oauth_token", "gho_OAUTH123")
+	case "GH_TOKEN":
+		// classic style token stored in config (will fail prefix check)
+		c.Set("github.com", "oauth_token", "ghp_CLASSIC123")
+	case "GH_ENTERPRISE_TOKEN":
+		// enterprise style token stored in config (will fail prefix check)
+		c.Set("something.ghes.com", "oauth_token", "ghe_ENTERPRISE123")
+	}
+	return c
+}
+
+func TestNewCmdAgentTask(t *testing.T) {
+	tests := []struct {
+		name            string
+		tokenSource     string
+		customConfig    func() (gh.Config, error)
+		wantErr         bool
+		wantErrContains string
+		wantStdout      string
+	}{
+		{
+			name:        "oauth token is accepted",
+			tokenSource: "oauth_token",
+			wantErr:     false,
+			wantStdout:  "",
+		},
+		{
+			name:        "keyring oauth token is accepted",
+			tokenSource: "keyring",
+			wantErr:     false,
+			wantStdout:  "",
+		},
+		{
+			name:            "env var token is rejected",
+			tokenSource:     "GH_TOKEN",
+			wantErr:         true,
+			wantErrContains: "requires an OAuth token",
+		},
+		{
+			name:        "enterprise token alone is ignored and rejected",
+			tokenSource: "GH_ENTERPRISE_TOKEN",
+			wantErr:     true,
+		},
+		{
+			name: "github.com oauth is accepted and enterprise token ignored",
+			customConfig: func() (gh.Config, error) {
+				c := config.NewBlankConfig()
+				c.Set("something.ghes.com", "oauth_token", "ghe_ENTERPRISE123")
+				c.Set("github.com", "oauth_token", "gho_OAUTH123")
+				return c, nil
+			},
+			wantErr:    false,
+			wantStdout: "",
+		},
+		{
+			name: "enterprise host is rejected",
+			customConfig: func() (gh.Config, error) {
+				return &ghmock.ConfigMock{
+					AuthenticationFunc: func() gh.AuthConfig {
+						c := &config.AuthConfig{}
+						c.SetDefaultHost("something.ghes.com", "GH_HOST")
+						return c
+					},
+				}, nil
+			},
+			wantErr:         true,
+			wantErrContains: "not supported on this host",
+		},
+		{
+			name: "empty host is rejected",
+			customConfig: func() (gh.Config, error) {
+				return &ghmock.ConfigMock{
+					AuthenticationFunc: func() gh.AuthConfig {
+						c := &config.AuthConfig{}
+						c.SetDefaultHost("", "GH_HOST")
+						return c
+					},
+				}, nil
+			},
+			wantErr:         true,
+			wantErrContains: "no default host configured",
+		},
+		{
+			name:        "no auth is rejected",
+			tokenSource: "",
+			wantErr:     true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			f := &cmdutil.Factory{}
+			ios, _, stdout, _ := iostreams.Test()
+			f.IOStreams = ios
+			if tt.customConfig != nil {
+				f.Config = tt.customConfig
+			} else {
+				f.Config = func() (gh.Config, error) { return setupMockOAuthConfig(t, tt.tokenSource), nil }
+			}
+
+			cmd := NewCmdAgentTask(f)
+			err := cmd.Execute()
+
+			if tt.wantErr {
+				require.Error(t, err)
+				if tt.wantErrContains != "" {
+					require.Contains(t, err.Error(), tt.wantErrContains)
+				}
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tt.wantStdout, stdout.String())
+			}
+		})
+	}
+}
+
+func TestAliasAreSet(t *testing.T) {
+	f := &cmdutil.Factory{}
+	ios, _, _, _ := iostreams.Test()
+	f.IOStreams = ios
+	f.Config = func() (gh.Config, error) { return setupMockOAuthConfig(t, "oauth_token"), nil }
+
+	cmd := NewCmdAgentTask(f)
+
+	require.ElementsMatch(t, []string{"agent-tasks", "agent", "agents"}, cmd.Aliases)
+}
diff --git a/pkg/cmd/agent-task/capi/job_test.go b/pkg/cmd/agent-task/capi/job_test.go
new file mode 100644
index 000000000..573ce3039
--- /dev/null
+++ b/pkg/cmd/agent-task/capi/job_test.go
@@ -0,0 +1,379 @@
+package capi
+
+import (
+	"context"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/internal/config"
+	"github.com/cli/cli/v2/pkg/httpmock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetJobRequiresRepoAndJobID(t *testing.T) {
+	client := &CAPIClient{}
+	_, err := client.GetJob(context.Background(), "", "", "only-job-id")
+	assert.EqualError(t, err, "owner, repo, and jobID are required")
+	_, err = client.GetJob(context.Background(), "", "only-repo", "")
+	assert.EqualError(t, err, "owner, repo, and jobID are required")
+	_, err = client.GetJob(context.Background(), "only-owner", "", "")
+	assert.EqualError(t, err, "owner, repo, and jobID are required")
+	_, err = client.GetJob(context.Background(), "", "", "")
+	assert.EqualError(t, err, "owner, repo, and jobID are required")
+}
+
+func TestGetJob(t *testing.T) {
+	sampleDateString := "2025-08-29T00:00:00Z"
+	sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name      string
+		httpStubs func(*testing.T, *httpmock.Registry)
+		wantErr   string
+		wantOut   *Job
+	}{
+		{
+			name: "job without PR",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(200, heredoc.Docf(`
+						{
+							"job_id": "job123",
+							"session_id": "sess1",
+							"problem_statement": "Do the thing",
+							"event_type": "foo",
+							"content_filter_mode": "foo",
+							"status": "foo",
+							"result": "foo",
+							"actor": {
+								"id": 1,
+								"login": "octocat"
+							},
+							"created_at": "%[1]s",
+							"updated_at": "%[1]s"
+						}`,
+						sampleDateString,
+					)),
+				)
+			},
+			wantOut: &Job{
+				ID:                "job123",
+				SessionID:         "sess1",
+				ProblemStatement:  "Do the thing",
+				EventType:         "foo",
+				ContentFilterMode: "foo",
+				Status:            "foo",
+				Result:            "foo",
+				Actor: &JobActor{
+					ID:    1,
+					Login: "octocat",
+				},
+				CreatedAt: sampleDate,
+				UpdatedAt: sampleDate,
+			},
+		},
+		{
+			name: "job with PR",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(200, heredoc.Docf(`
+						{
+							"job_id": "job123",
+							"session_id": "sess1",
+							"problem_statement": "Do the thing",
+							"event_type": "foo",
+							"content_filter_mode": "foo",
+							"status": "foo",
+							"result": "foo",
+							"actor": {
+								"id": 1,
+								"login": "octocat"
+							},
+							"created_at": "%[1]s",
+							"updated_at": "%[1]s",
+							"pull_request": {
+								"id": 101,
+								"number": 42
+							}
+						}`,
+						sampleDateString,
+					)),
+				)
+			},
+			wantOut: &Job{
+				ID:                "job123",
+				SessionID:         "sess1",
+				ProblemStatement:  "Do the thing",
+				EventType:         "foo",
+				ContentFilterMode: "foo",
+				Status:            "foo",
+				Result:            "foo",
+				Actor: &JobActor{
+					ID:    1,
+					Login: "octocat",
+				},
+				CreatedAt: sampleDate,
+				UpdatedAt: sampleDate,
+				PullRequest: &JobPullRequest{
+					ID:     101,
+					Number: 42,
+				},
+			},
+		},
+		{
+			name: "job not found",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(404, `{}`),
+				)
+			},
+			wantErr: "failed to get job: 404 Not Found",
+		},
+		{
+			name: "API error",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(500, `{}`),
+				)
+			},
+			wantErr: "failed to get job: 500 Internal Server Error",
+		},
+		{
+			name: "invalid JSON response",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(200, ``),
+				)
+			},
+			wantErr: "failed to decode get job response: EOF",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			if tt.httpStubs != nil {
+				tt.httpStubs(t, reg)
+			}
+			defer reg.Verify(t)
+
+			httpClient := &http.Client{Transport: reg}
+
+			cfg := config.NewBlankConfig()
+			capiClient := NewCAPIClient(httpClient, cfg.Authentication())
+
+			job, err := capiClient.GetJob(context.Background(), "OWNER", "REPO", "job123")
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+				require.Nil(t, job)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantOut, job)
+		})
+	}
+}
+
+func TestCreateJobRequiresRepoAndProblemStatement(t *testing.T) {
+	client := &CAPIClient{}
+
+	_, err := client.CreateJob(context.Background(), "", "only-repo", "", "")
+	assert.EqualError(t, err, "owner and repo are required")
+	_, err = client.CreateJob(context.Background(), "only-owner", "", "", "")
+	assert.EqualError(t, err, "owner and repo are required")
+	_, err = client.CreateJob(context.Background(), "", "", "", "")
+	assert.EqualError(t, err, "owner and repo are required")
+
+	_, err = client.CreateJob(context.Background(), "owner", "repo", "", "")
+	assert.EqualError(t, err, "problem statement is required")
+}
+
+func TestCreateJob(t *testing.T) {
+	sampleDateString := "2025-08-29T00:00:00Z"
+	sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name       string
+		baseBranch string
+		httpStubs  func(*testing.T, *httpmock.Registry)
+		wantErr    string
+		wantOut    *Job
+	}{
+		{
+			name: "success",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
+					httpmock.RESTPayload(201,
+						heredoc.Docf(`
+							{
+								"job_id": "job123",
+								"session_id": "sess1",
+								"problem_statement": "Do the thing",
+								"event_type": "foo",
+								"content_filter_mode": "foo",
+								"status": "foo",
+								"result": "foo",
+								"actor": {
+									"id": 1,
+									"login": "octocat"
+								},
+								"created_at": "%[1]s",
+								"updated_at": "%[1]s"
+							}
+						`, sampleDateString),
+						func(payload map[string]interface{}) {
+							assert.Equal(t, "Do the thing", payload["problem_statement"])
+							assert.Equal(t, "gh_cli", payload["event_type"])
+						},
+					),
+				)
+			},
+			wantOut: &Job{
+				ID:                "job123",
+				SessionID:         "sess1",
+				ProblemStatement:  "Do the thing",
+				EventType:         "foo",
+				ContentFilterMode: "foo",
+				Status:            "foo",
+				Result:            "foo",
+				Actor: &JobActor{
+					ID:    1,
+					Login: "octocat",
+				},
+				CreatedAt: sampleDate,
+				UpdatedAt: sampleDate,
+			},
+		},
+		{
+			name:       "success with base branch",
+			baseBranch: "some-branch",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
+					httpmock.RESTPayload(201,
+						heredoc.Docf(`
+							{
+								"job_id": "job123",
+								"session_id": "sess1",
+								"problem_statement": "Do the thing",
+								"event_type": "foo",
+								"content_filter_mode": "foo",
+								"status": "foo",
+								"result": "foo",
+								"actor": {
+									"id": 1,
+									"login": "octocat"
+								},
+								"created_at": "%[1]s",
+								"updated_at": "%[1]s"
+							}
+						`, sampleDateString),
+						func(payload map[string]interface{}) {
+							assert.Equal(t, "Do the thing", payload["problem_statement"])
+							assert.Equal(t, "gh_cli", payload["event_type"])
+							assert.Equal(t, "refs/heads/some-branch", payload["pull_request"].(map[string]interface{})["base_ref"])
+						},
+					),
+				)
+			},
+			wantOut: &Job{
+				ID:                "job123",
+				SessionID:         "sess1",
+				ProblemStatement:  "Do the thing",
+				EventType:         "foo",
+				ContentFilterMode: "foo",
+				Status:            "foo",
+				Result:            "foo",
+				Actor: &JobActor{
+					ID:    1,
+					Login: "octocat",
+				},
+				CreatedAt: sampleDate,
+				UpdatedAt: sampleDate,
+			},
+		},
+		{
+			name: "API error, included in response body",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(500, heredoc.Doc(`{
+						"error": {
+							"message": "some error"
+						}
+					}`)),
+				)
+			},
+			wantErr: "failed to create job: some error",
+		},
+		{
+			name: "API error",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(500, `{}`),
+				)
+			},
+			wantErr: "failed to create job: 500 Internal Server Error",
+		},
+		{
+			name: "invalid JSON response, non-HTTP 200",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(401, `Unauthorized`),
+				)
+			},
+			wantErr: "failed to create job: 401 Unauthorized",
+		},
+		{
+			name: "invalid JSON response, HTTP 200",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(200, ``),
+				)
+			},
+			wantErr: "failed to decode create job response: EOF",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			if tt.httpStubs != nil {
+				tt.httpStubs(t, reg)
+			}
+			defer reg.Verify(t)
+
+			httpClient := &http.Client{Transport: reg}
+
+			cfg := config.NewBlankConfig()
+			capiClient := NewCAPIClient(httpClient, cfg.Authentication())
+
+			job, err := capiClient.CreateJob(context.Background(), "OWNER", "REPO", "Do the thing", tt.baseBranch)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+				require.Nil(t, job)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantOut, job)
+		})
+	}
+}
diff --git a/pkg/cmd/agent-task/capi/sessions_test.go b/pkg/cmd/agent-task/capi/sessions_test.go
new file mode 100644
index 000000000..848fd716a
--- /dev/null
+++ b/pkg/cmd/agent-task/capi/sessions_test.go
@@ -0,0 +1,1919 @@
+package capi
+
+import (
+	"context"
+	"net/http"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/api"
+
+	"github.com/cli/cli/v2/internal/config"
+	"github.com/cli/cli/v2/pkg/httpmock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestListLatestSessionsForViewer(t *testing.T) {
+	sampleDateString := "2025-08-29T00:00:00Z"
+	sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name      string
+		perPage   int
+		limit     int
+		httpStubs func(*testing.T, *httpmock.Registry)
+		wantErr   string
+		wantOut   []*Session
+	}{
+		{
+			name:    "zero limit",
+			limit:   0,
+			wantOut: nil,
+		},
+		{
+			name:  "no sessions",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"50"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(`{"sessions":[]}`),
+				)
+			},
+			wantOut: nil,
+		},
+		{
+			name:  "single session",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"50"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sess1",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 2000,
+									"created_at": "%[1]s",
+									"premium_requests": 0.1
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node",
+										"fullDatabaseId": "2000",
+										"number": 42,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/42",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {
+											"nameWithOwner": "OWNER/REPO"
+										}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNB9A", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+
+					ID:              "sess1",
+					Name:            "Build artifacts",
+					UserID:          1,
+					AgentID:         2,
+					Logs:            "",
+					State:           "completed",
+					OwnerID:         10,
+					RepoID:          1000,
+					ResourceType:    "pull",
+					ResourceID:      2000,
+					CreatedAt:       sampleDate,
+					PremiumRequests: 0.1,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node",
+						FullDatabaseID: "2000",
+						Number:         42,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/42",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{
+						Login:      "octocat",
+						Name:       "Octocat",
+						DatabaseID: 1,
+					},
+				},
+			},
+		},
+		{
+			// This happens at the early moments of a session lifecycle, before a PR is created and associated with it.
+			name:  "single session, no pull request resource",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"50"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sess1",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "",
+									"resource_id": 0,
+									"created_at": "%[1]s",
+									"premium_requests": 0.1
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						assert.Equal(t, []interface{}{"U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+
+					ID:              "sess1",
+					Name:            "Build artifacts",
+					UserID:          1,
+					AgentID:         2,
+					Logs:            "",
+					State:           "completed",
+					OwnerID:         10,
+					RepoID:          1000,
+					ResourceType:    "",
+					ResourceID:      0,
+					CreatedAt:       sampleDate,
+					PremiumRequests: 0.1,
+					User: &api.GitHubUser{
+						Login:      "octocat",
+						Name:       "Octocat",
+						DatabaseID: 1,
+					},
+				},
+			},
+		},
+		{
+			name:    "multiple sessions, paginated",
+			perPage: 1, // to enforce pagination
+			limit:   2,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"1"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sess1",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 2000,
+									"created_at": "%[1]s",
+									"premium_requests": 0.1
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+
+				// Second page
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"2"},
+							"page_size":   {"1"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sess2",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 2001,
+									"created_at": "%[1]s",
+									"premium_requests": 0.1
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node",
+										"fullDatabaseId": "2000",
+										"number": 42,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/42",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {
+											"nameWithOwner": "OWNER/REPO"
+										}
+									},
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node",
+										"fullDatabaseId": "2001",
+										"number": 43,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/43",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {
+											"nameWithOwner": "OWNER/REPO"
+										}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNB9A", "PR_kwDNA-jNB9E", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+					ID:              "sess1",
+					Name:            "Build artifacts",
+					UserID:          1,
+					AgentID:         2,
+					Logs:            "",
+					State:           "completed",
+					OwnerID:         10,
+					RepoID:          1000,
+					ResourceType:    "pull",
+					ResourceID:      2000,
+					CreatedAt:       sampleDate,
+					PremiumRequests: 0.1,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node",
+						FullDatabaseID: "2000",
+						Number:         42,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/42",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{
+						Login:      "octocat",
+						Name:       "Octocat",
+						DatabaseID: 1,
+					},
+				},
+				{
+					ID:              "sess2",
+					Name:            "Build artifacts",
+					UserID:          1,
+					AgentID:         2,
+					Logs:            "",
+					State:           "completed",
+					OwnerID:         10,
+					RepoID:          1000,
+					ResourceType:    "pull",
+					ResourceID:      2001,
+					CreatedAt:       sampleDate,
+					PremiumRequests: 0.1,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node",
+						FullDatabaseID: "2001",
+						Number:         43,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/43",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{
+						Login:      "octocat",
+						Name:       "Octocat",
+						DatabaseID: 1,
+					},
+				},
+			},
+		},
+		{
+			name:    "multiple pages with duplicates per PR only newest kept",
+			perPage: 2,
+			limit:   3,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				// Page 1 returns newest sessions (ordered newest first overall)
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"2"},
+							"sort":        {"last_updated_at,desc"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sessA-new",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3000,
+									"created_at": "%[1]s"
+								},
+								{
+									"id": "sessB-new",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3001,
+									"created_at": "%[1]s"
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+
+				// Page 2 returns older duplicate sessions for 3000, plus another new PR 3002
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"2"},
+							"page_size":   {"2"},
+							"sort":        {"last_updated_at,desc"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sessA-old",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3000,
+									"created_at": "%[1]s"
+								},
+								{
+									"id": "sessC-new",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3002,
+									"created_at": "%[1]s"
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+
+				// GraphQL hydration for PRs 3000, 3001, 3002 and user 1
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node3000",
+										"fullDatabaseId": "3000",
+										"number": 100,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/100",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {"nameWithOwner": "OWNER/REPO"}
+									},
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node3001",
+										"fullDatabaseId": "3001",
+										"number": 101,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/101",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {"nameWithOwner": "OWNER/REPO"}
+									},
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node3002",
+										"fullDatabaseId": "3002",
+										"number": 102,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/102",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {"nameWithOwner": "OWNER/REPO"}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						// Expected encoded node IDs for resource IDs 3000,3001,3002 and user octocat
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNC7g", "PR_kwDNA-jNC7k", "PR_kwDNA-jNC7o", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+					ID:           "sessA-new",
+					Name:         "Build artifacts",
+					UserID:       1,
+					AgentID:      2,
+					Logs:         "",
+					State:        "completed",
+					OwnerID:      10,
+					RepoID:       1000,
+					ResourceType: "pull",
+					ResourceID:   3000,
+					CreatedAt:    sampleDate,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node3000",
+						FullDatabaseID: "3000",
+						Number:         100,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/100",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+				{
+					ID:           "sessB-new",
+					Name:         "Build artifacts",
+					UserID:       1,
+					AgentID:      2,
+					Logs:         "",
+					State:        "completed",
+					OwnerID:      10,
+					RepoID:       1000,
+					ResourceType: "pull",
+					ResourceID:   3001,
+					CreatedAt:    sampleDate,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node3001",
+						FullDatabaseID: "3001",
+						Number:         101,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/101",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+				{
+					ID:           "sessC-new",
+					Name:         "Build artifacts",
+					UserID:       1,
+					AgentID:      2,
+					Logs:         "",
+					State:        "completed",
+					OwnerID:      10,
+					RepoID:       1000,
+					ResourceType: "pull",
+					ResourceID:   3002,
+					CreatedAt:    sampleDate,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node3002",
+						FullDatabaseID: "3002",
+						Number:         102,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/102",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+			},
+		},
+		{
+			name:    "multiple pages with zero resource IDs all kept",
+			perPage: 2,
+			limit:   3,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				// Page 1 returns newest sessions, one with a zero resource ID
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"2"},
+							"sort":        {"last_updated_at,desc"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sessA-new",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3000,
+									"created_at": "%[1]s"
+								},
+								{
+									"id": "sessB-new",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "queued",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "",
+									"resource_id": 0,
+									"created_at": "%[1]s"
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+
+				// Page 2 returns older duplicate sessions for 3000, plus another new session with zero resource ID
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"2"},
+							"page_size":   {"2"},
+							"sort":        {"last_updated_at,desc"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sessA-old",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3000,
+									"created_at": "%[1]s"
+								},
+								{
+									"id": "sessC-new",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "queued",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "",
+									"resource_id": 0,
+									"created_at": "%[1]s"
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+
+				// GraphQL hydration for PRs 3000 and user 1
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node3000",
+										"fullDatabaseId": "3000",
+										"number": 100,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/100",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {"nameWithOwner": "OWNER/REPO"}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						// Expected encoded node IDs for resource IDs 3000 and user octocat
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNC7g", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+					ID:           "sessA-new",
+					Name:         "Build artifacts",
+					UserID:       1,
+					AgentID:      2,
+					Logs:         "",
+					State:        "completed",
+					OwnerID:      10,
+					RepoID:       1000,
+					ResourceType: "pull",
+					ResourceID:   3000,
+					CreatedAt:    sampleDate,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node3000",
+						FullDatabaseID: "3000",
+						Number:         100,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/100",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+				{
+					ID:           "sessB-new",
+					Name:         "Build artifacts",
+					UserID:       1,
+					AgentID:      2,
+					Logs:         "",
+					State:        "queued",
+					OwnerID:      10,
+					RepoID:       1000,
+					ResourceType: "",
+					ResourceID:   0,
+					CreatedAt:    sampleDate,
+					User:         &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+				{
+					ID:           "sessC-new",
+					Name:         "Build artifacts",
+					UserID:       1,
+					AgentID:      2,
+					Logs:         "",
+					State:        "queued",
+					OwnerID:      10,
+					RepoID:       1000,
+					ResourceType: "",
+					ResourceID:   0,
+					CreatedAt:    sampleDate,
+					User:         &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+			},
+		},
+		{
+			name:  "session error is included",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"50"},
+							"sort":        {"last_updated_at,desc"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sessA",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "failed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3000,
+									"created_at": "%[1]s",
+									"error": {
+										"code": "some-error-code",
+										"message": "some-error-message"
+									}
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node3000",
+										"fullDatabaseId": "3000",
+										"number": 100,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/100",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {"nameWithOwner": "OWNER/REPO"}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						// Expected encoded node IDs for resource IDs 3000 and user octocat
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNC7g", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+					ID:           "sessA",
+					Name:         "Build artifacts",
+					UserID:       1,
+					AgentID:      2,
+					Logs:         "",
+					State:        "failed",
+					OwnerID:      10,
+					RepoID:       1000,
+					ResourceType: "pull",
+					ResourceID:   3000,
+					CreatedAt:    sampleDate,
+					Error: &SessionError{
+						Code:    "some-error-code",
+						Message: "some-error-message",
+					},
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node3000",
+						FullDatabaseID: "3000",
+						Number:         100,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/100",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+			},
+		},
+		{
+			name:  "workflow run id is included",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"50"},
+							"sort":        {"last_updated_at,desc"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sessA",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "failed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 3000,
+									"created_at": "%[1]s",
+									"workflow_run_id": 9999
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node3000",
+										"fullDatabaseId": "3000",
+										"number": 100,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/100",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {"nameWithOwner": "OWNER/REPO"}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						// Expected encoded node IDs for resource IDs 3000 and user octocat
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNC7g", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+					ID:            "sessA",
+					Name:          "Build artifacts",
+					UserID:        1,
+					AgentID:       2,
+					Logs:          "",
+					State:         "failed",
+					OwnerID:       10,
+					RepoID:        1000,
+					ResourceType:  "pull",
+					ResourceID:    3000,
+					CreatedAt:     sampleDate,
+					WorkflowRunID: 9999,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node3000",
+						FullDatabaseID: "3000",
+						Number:         100,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/100",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{Login: "octocat", Name: "Octocat", DatabaseID: 1},
+				},
+			},
+		},
+		{
+			name:  "API error",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"50"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StatusStringResponse(500, "{}"),
+				)
+			},
+			wantErr: "failed to list sessions:",
+		}, {
+			name:  "API error at hydration",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(
+						httpmock.QueryMatcher("GET", "agents/sessions", url.Values{
+							"page_number": {"1"},
+							"page_size":   {"50"},
+						}),
+						"api.githubcopilot.com",
+					),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sess1",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 2000,
+									"created_at": "%[1]s",
+									"premium_requests": 0.1
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.StatusStringResponse(500, `{}`),
+				)
+			},
+			wantErr: `failed to fetch session resources: non-200 OK status code:`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			if tt.httpStubs != nil {
+				tt.httpStubs(t, reg)
+			}
+			defer reg.Verify(t)
+
+			httpClient := &http.Client{Transport: reg}
+
+			cfg := config.NewBlankConfig()
+			capiClient := NewCAPIClient(httpClient, cfg.Authentication())
+
+			if tt.perPage != 0 {
+				last := defaultSessionsPerPage
+				defaultSessionsPerPage = tt.perPage
+				defer func() {
+					defaultSessionsPerPage = last
+				}()
+			}
+
+			sessions, err := capiClient.ListLatestSessionsForViewer(context.Background(), tt.limit)
+
+			if tt.wantErr != "" {
+				require.ErrorContains(t, err, tt.wantErr)
+				require.Nil(t, sessions)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantOut, sessions)
+		})
+	}
+}
+
+func TestListSessionsByResourceIDRequiresResource(t *testing.T) {
+	client := &CAPIClient{}
+
+	_, err := client.ListSessionsByResourceID(context.Background(), "", 999, 0)
+	assert.EqualError(t, err, "missing resource type/ID")
+	_, err = client.ListSessionsByResourceID(context.Background(), "only-resource-type", 0, 0)
+	assert.EqualError(t, err, "missing resource type/ID")
+	_, err = client.ListSessionsByResourceID(context.Background(), "", 0, 0)
+	assert.EqualError(t, err, "missing resource type/ID")
+}
+
+func TestListSessionsByResourceID(t *testing.T) {
+	sampleDateString := "2025-08-29T07:00:00Z"
+	sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
+	require.NoError(t, err)
+	sampleDateTimestamp := sampleDate.Unix()
+
+	resourceID := int64(999)
+	resourceType := "pull"
+
+	tests := []struct {
+		name      string
+		perPage   int
+		limit     int
+		httpStubs func(*testing.T, *httpmock.Registry)
+		wantErr   string
+		wantOut   []*Session
+	}{
+		{
+			name:    "zero limit",
+			limit:   0,
+			wantOut: nil,
+		},
+		{
+			// If the given pull request does not exist or the pull request has no sessions,
+			// the API endpoint returns 404 with different messages. We should treat them
+			// the same though.
+			name:  "no sessions or no pull request",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/resource/pull/999"), "api.githubcopilot.com"),
+
+					httpmock.StatusStringResponse(404, "{}"),
+				)
+			},
+			wantErr: "failed to list sessions",
+		},
+		{
+			name:  "single session",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/resource/pull/999"), "api.githubcopilot.com"),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"id": "resource:pull:2000",
+							"user_id": 1,
+							"resource_global_id": "PR_kwDNA-jNB9A",
+							"resource_type": "pull",
+							"resource_id": 2000,
+							"session_count": 1,
+							"last_updated_at": %[1]d,
+							"state": "completed",
+							"resource_state": "draft",
+							"sessions": [
+								{
+									"id": "sess1",
+									"name": "Build artifacts",
+									"state": "completed",
+									"last_updated_at": %[1]d
+								}
+							]
+						}`,
+						sampleDateTimestamp,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node",
+										"fullDatabaseId": "2000",
+										"number": 42,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/42",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {
+											"nameWithOwner": "OWNER/REPO"
+										}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNB9A", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+					ID:            "sess1",
+					CreatedAt:     time.Time{},
+					LastUpdatedAt: sampleDate,
+					Name:          "Build artifacts",
+					UserID:        1,
+					State:         "completed",
+					ResourceType:  "pull",
+					ResourceID:    2000,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node",
+						FullDatabaseID: "2000",
+						Number:         42,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/42",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{
+						Login:      "octocat",
+						Name:       "Octocat",
+						DatabaseID: 1,
+					},
+				},
+			},
+		},
+		{
+			name:    "multiple sessions",
+			perPage: 1, // to enforce pagination
+			limit:   2,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/resource/pull/999"), "api.githubcopilot.com"),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"id": "resource:pull:2000",
+							"user_id": 1,
+							"resource_global_id": "PR_kwDNA-jNB9A",
+							"resource_type": "pull",
+							"resource_id": 2000,
+							"session_count": 1,
+							"last_updated_at": %[1]d,
+							"state": "completed",
+							"resource_state": "draft",
+							"sessions": [
+								{
+									"id": "sess1",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 2000,
+									"created_at": %[1]d,
+									"premium_requests": 0.1
+								},
+								{
+									"id": "sess2",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 2001,
+									"created_at": %[1]d,
+									"premium_requests": 0.1
+								}
+							]
+						}`,
+						sampleDateTimestamp,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node",
+										"fullDatabaseId": "2000",
+										"number": 42,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/42",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {
+											"nameWithOwner": "OWNER/REPO"
+										}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNB9A", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: []*Session{
+				{
+					ID:           "sess1",
+					Name:         "Build artifacts",
+					UserID:       1,
+					State:        "completed",
+					ResourceType: "pull",
+					ResourceID:   2000,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node",
+						FullDatabaseID: "2000",
+						Number:         42,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/42",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{
+						Login:      "octocat",
+						Name:       "Octocat",
+						DatabaseID: 1,
+					},
+				},
+				{
+					ID:           "sess2",
+					Name:         "Build artifacts",
+					UserID:       1,
+					State:        "completed",
+					ResourceType: "pull",
+					ResourceID:   2000,
+					PullRequest: &api.PullRequest{
+						ID:             "PR_node",
+						FullDatabaseID: "2000",
+						Number:         42,
+						Title:          "Improve docs",
+						State:          "OPEN",
+						IsDraft:        true,
+						URL:            "https://github.com/OWNER/REPO/pull/42",
+						Body:           "",
+						CreatedAt:      sampleDate,
+						UpdatedAt:      sampleDate,
+						Repository: &api.PRRepository{
+							NameWithOwner: "OWNER/REPO",
+						},
+					},
+					User: &api.GitHubUser{
+						Login:      "octocat",
+						Name:       "Octocat",
+						DatabaseID: 1,
+					},
+				},
+			},
+		},
+		{
+			name:  "API error",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/resource/pull/999"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(500, "{}"),
+				)
+			},
+			wantErr: "failed to list sessions:",
+		}, {
+			name:  "API error at hydration",
+			limit: 10,
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/resource/pull/999"), "api.githubcopilot.com"),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"sessions": [
+								{
+									"id": "sess1",
+									"name": "Build artifacts",
+									"user_id": 1,
+									"agent_id": 2,
+									"logs": "",
+									"state": "completed",
+									"owner_id": 10,
+									"repo_id": 1000,
+									"resource_type": "pull",
+									"resource_id": 2000,
+									"created_at": "%[1]s",
+									"premium_requests": 0.1
+								}
+							]
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.StatusStringResponse(500, `{}`),
+				)
+			},
+			wantErr: `failed to fetch session resources: non-200 OK status code:`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			if tt.httpStubs != nil {
+				tt.httpStubs(t, reg)
+			}
+			defer reg.Verify(t)
+
+			httpClient := &http.Client{Transport: reg}
+
+			cfg := config.NewBlankConfig()
+			capiClient := NewCAPIClient(httpClient, cfg.Authentication())
+
+			if tt.perPage != 0 {
+				last := defaultSessionsPerPage
+				defaultSessionsPerPage = tt.perPage
+				defer func() {
+					defaultSessionsPerPage = last
+				}()
+			}
+
+			sessions, err := capiClient.ListSessionsByResourceID(context.Background(), resourceType, resourceID, tt.limit)
+
+			if tt.wantErr != "" {
+				require.ErrorContains(t, err, tt.wantErr)
+				require.Nil(t, sessions)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantOut, sessions)
+		})
+	}
+}
+
+func TestGetSessionRequiresID(t *testing.T) {
+	client := &CAPIClient{}
+
+	_, err := client.GetSession(context.Background(), "")
+	assert.EqualError(t, err, "missing session ID")
+}
+
+func TestGetSession(t *testing.T) {
+	sampleDateString := "2025-08-29T00:00:00Z"
+	sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name      string
+		httpStubs func(*testing.T, *httpmock.Registry)
+		wantErr   string
+		wantErrIs error
+		wantOut   *Session
+	}{
+		{
+			name: "session not found",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/sessions/some-uuid"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(404, "{}"),
+				)
+			},
+			wantErrIs: ErrSessionNotFound,
+			wantErr:   "not found",
+		},
+		{
+			name: "API error",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/sessions/some-uuid"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(500, "some error"),
+				)
+			},
+			wantErr: "failed to get session:",
+		},
+		{
+			name: "invalid JSON response",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/sessions/some-uuid"), "api.githubcopilot.com"),
+					httpmock.StatusStringResponse(200, ""),
+				)
+			},
+			wantErr: "failed to decode session response: EOF",
+		},
+		{
+			name: "success",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/sessions/some-uuid"), "api.githubcopilot.com"),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"id": "some-uuid",
+							"name": "Build artifacts",
+							"user_id": 1,
+							"agent_id": 2,
+							"logs": "",
+							"state": "completed",
+							"owner_id": 10,
+							"repo_id": 1000,
+							"resource_type": "pull",
+							"resource_id": 2000,
+							"created_at": "%[1]s",
+							"premium_requests": 0.1
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "PullRequest",
+										"id": "PR_node",
+										"fullDatabaseId": "2000",
+										"number": 42,
+										"title": "Improve docs",
+										"state": "OPEN",
+										"isDraft": true,
+										"url": "https://github.com/OWNER/REPO/pull/42",
+										"body": "",
+										"createdAt": "%[1]s",
+										"updatedAt": "%[1]s",
+										"repository": {
+											"nameWithOwner": "OWNER/REPO"
+										}
+									},
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						assert.Equal(t, []interface{}{"PR_kwDNA-jNB9A", "U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: &Session{
+				ID:              "some-uuid",
+				Name:            "Build artifacts",
+				UserID:          1,
+				AgentID:         2,
+				Logs:            "",
+				State:           "completed",
+				OwnerID:         10,
+				RepoID:          1000,
+				ResourceType:    "pull",
+				ResourceID:      2000,
+				CreatedAt:       sampleDate,
+				PremiumRequests: 0.1,
+				PullRequest: &api.PullRequest{
+					ID:             "PR_node",
+					FullDatabaseID: "2000",
+					Number:         42,
+					Title:          "Improve docs",
+					State:          "OPEN",
+					IsDraft:        true,
+					URL:            "https://github.com/OWNER/REPO/pull/42",
+					Body:           "",
+					CreatedAt:      sampleDate,
+					UpdatedAt:      sampleDate,
+					Repository: &api.PRRepository{
+						NameWithOwner: "OWNER/REPO",
+					},
+				},
+				User: &api.GitHubUser{
+					Login:      "octocat",
+					Name:       "Octocat",
+					DatabaseID: 1,
+				},
+			},
+		},
+		{
+			// This happens at the early moments of a session lifecycle, before a PR is created and associated with it.
+			name: "success, but no pull request resource",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/sessions/some-uuid"), "api.githubcopilot.com"),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"id": "some-uuid",
+							"name": "Build artifacts",
+							"user_id": 1,
+							"agent_id": 2,
+							"logs": "",
+							"state": "completed",
+							"owner_id": 10,
+							"repo_id": 1000,
+							"resource_type": "",
+							"resource_id": 0,
+							"created_at": "%[1]s",
+							"premium_requests": 0.1
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.GraphQLQuery(heredoc.Docf(`
+						{
+							"data": {
+								"nodes": [
+									{
+										"__typename": "User",
+										"login": "octocat",
+										"name": "Octocat",
+										"databaseId": 1
+									}
+								]
+							}
+						}`,
+						sampleDateString,
+					), func(q string, vars map[string]interface{}) {
+						assert.Equal(t, []interface{}{"U_kgAB"}, vars["ids"])
+					}),
+				)
+			},
+			wantOut: &Session{
+				ID:              "some-uuid",
+				Name:            "Build artifacts",
+				UserID:          1,
+				AgentID:         2,
+				Logs:            "",
+				State:           "completed",
+				OwnerID:         10,
+				RepoID:          1000,
+				ResourceType:    "",
+				ResourceID:      0,
+				CreatedAt:       sampleDate,
+				PremiumRequests: 0.1,
+				User: &api.GitHubUser{
+					Login:      "octocat",
+					Name:       "Octocat",
+					DatabaseID: 1,
+				},
+			},
+		},
+		{
+			name: "API error at hydration",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.REST("GET", "agents/sessions/some-uuid"), "api.githubcopilot.com"),
+					httpmock.StringResponse(heredoc.Docf(`
+						{
+							"id": "some-uuid",
+							"name": "Build artifacts",
+							"user_id": 1,
+							"agent_id": 2,
+							"logs": "",
+							"state": "completed",
+							"owner_id": 10,
+							"repo_id": 1000,
+							"resource_type": "pull",
+							"resource_id": 2000,
+							"created_at": "%[1]s",
+							"premium_requests": 0.1
+						}`,
+						sampleDateString,
+					)),
+				)
+				// GraphQL hydration
+				reg.Register(
+					httpmock.GraphQL(`query FetchPRsAndUsersForAgentTaskSessions\b`),
+					httpmock.StatusStringResponse(500, `{}`),
+				)
+			},
+			wantErr: `failed to fetch session resources: non-200 OK status code:`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			if tt.httpStubs != nil {
+				tt.httpStubs(t, reg)
+			}
+			defer reg.Verify(t)
+
+			httpClient := &http.Client{Transport: reg}
+
+			cfg := config.NewBlankConfig()
+			capiClient := NewCAPIClient(httpClient, cfg.Authentication())
+
+			session, err := capiClient.GetSession(context.Background(), "some-uuid")
+
+			if tt.wantErrIs != nil {
+				require.ErrorIs(t, err, tt.wantErrIs)
+			}
+
+			if tt.wantErr != "" {
+				require.ErrorContains(t, err, tt.wantErr)
+				require.Nil(t, session)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantOut, session)
+		})
+	}
+}
+func TestGetPullRequestDatabaseID(t *testing.T) {
+	tests := []struct {
+		name           string
+		httpStubs      func(*testing.T, *httpmock.Registry)
+		wantErr        string
+		wantDatabaseID int64
+		wantURL        string
+	}{
+		{
+			name: "graphql error",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.GraphQL(`query GetPullRequestFullDatabaseID\b`), "api.github.com"),
+					httpmock.StringResponse(`{"data":{}, "errors": [{"message": "some gql error"}]}`),
+				)
+			},
+			wantErr: "some gql error",
+		},
+		{
+			// This never happens in practice and it's just to cover more code path
+			name: "non-int database ID",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.GraphQL(`query GetPullRequestFullDatabaseID\b`), "api.github.com"),
+					httpmock.StringResponse(`{"data": {"repository": {"pullRequest": {"fullDatabaseId": "non-int", "url": "some-url"}}}}`),
+				)
+			},
+			wantErr: `strconv.ParseInt: parsing "non-int": invalid syntax`,
+			wantURL: "some-url",
+		},
+		{
+			name: "success",
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.WithHost(httpmock.GraphQL(`query GetPullRequestFullDatabaseID\b`), "api.github.com"),
+					httpmock.GraphQLQuery(`{"data": {"repository": {"pullRequest": {"fullDatabaseId": "999", "url": "some-url"}}}}`, func(s string, m map[string]interface{}) {
+						assert.Equal(t, "OWNER", m["owner"])
+						assert.Equal(t, "REPO", m["repo"])
+						assert.Equal(t, float64(42), m["number"])
+					}),
+				)
+			},
+			wantDatabaseID: 999,
+			wantURL:        "some-url",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			if tt.httpStubs != nil {
+				tt.httpStubs(t, reg)
+			}
+			defer reg.Verify(t)
+
+			httpClient := &http.Client{Transport: reg}
+
+			cfg := config.NewBlankConfig()
+			capiClient := NewCAPIClient(httpClient, cfg.Authentication())
+
+			databaseID, url, err := capiClient.GetPullRequestDatabaseID(context.Background(), "github.com", "OWNER", "REPO", 42)
+
+			if tt.wantErr != "" {
+				require.ErrorContains(t, err, tt.wantErr)
+				require.Zero(t, databaseID)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantDatabaseID, databaseID)
+			require.Equal(t, tt.wantURL, url)
+		})
+	}
+}
diff --git a/pkg/cmd/agent-task/create/create_test.go b/pkg/cmd/agent-task/create/create_test.go
new file mode 100644
index 000000000..ef7f529ac
--- /dev/null
+++ b/pkg/cmd/agent-task/create/create_test.go
@@ -0,0 +1,542 @@
+package create
+
+import (
+	"context"
+	"errors"
+	"io"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cenkalti/backoff/v4"
+	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/google/shlex"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewCmdCreate(t *testing.T) {
+	tests := []struct {
+		name     string
+		args     string
+		tty      bool
+		wantOpts *CreateOptions
+		wantErr  string
+	}{
+		{
+			name: "no args nor file returns no error (prompting path)",
+			tty:  true,
+			wantOpts: &CreateOptions{
+				ProblemStatement:     "",
+				ProblemStatementFile: "",
+			},
+		},
+		{
+			name: "arg only success",
+			args: "'task description from args'",
+			wantOpts: &CreateOptions{
+				ProblemStatement:     "task description from args",
+				ProblemStatementFile: "",
+			},
+		},
+		{
+			name:    "empty arg",
+			args:    "''",
+			wantErr: "task description cannot be empty",
+		},
+		{
+			name:    "whitespace arg",
+			args:    "'   '",
+			wantErr: "task description cannot be empty",
+		},
+		{
+			name:    "whitespace and newline arg",
+			args:    "'\n'",
+			wantErr: "task description cannot be empty",
+		},
+		{
+			name:    "mutually exclusive arg and file",
+			args:    "'some task inline' -F foo.md",
+			wantErr: "only one of -F or arg can be provided",
+		},
+		{
+			name: "base branch sets baseBranch field",
+			args: "'task description' -b feature",
+			wantOpts: &CreateOptions{
+				ProblemStatement:     "task description",
+				ProblemStatementFile: "",
+				BaseBranch:           "feature",
+			},
+		},
+		{
+			name: "with --follow",
+			args: "'task description from args' --follow",
+			wantOpts: &CreateOptions{
+				ProblemStatement:     "task description from args",
+				ProblemStatementFile: "",
+				Follow:               true,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ios, stdin, _, _ := iostreams.Test()
+			if tt.tty {
+				ios.SetStdinTTY(true)
+				ios.SetStdoutTTY(true)
+				ios.SetStderrTTY(true)
+			}
+			f := &cmdutil.Factory{IOStreams: ios}
+
+			var gotOpts *CreateOptions
+			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
+				gotOpts = o
+				return nil
+			})
+
+			argv, err := shlex.Split(tt.args)
+			require.NoError(t, err)
+			cmd.SetArgs(argv)
+			cmd.SetIn(stdin)
+			cmd.SetOut(io.Discard)
+			cmd.SetErr(io.Discard)
+
+			_, err = cmd.ExecuteC()
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			if tt.wantOpts != nil {
+				require.Equal(t, tt.wantOpts.ProblemStatement, gotOpts.ProblemStatement)
+				require.Equal(t, tt.wantOpts.ProblemStatementFile, gotOpts.ProblemStatementFile)
+				require.Equal(t, tt.wantOpts.BaseBranch, gotOpts.BaseBranch)
+			}
+		})
+	}
+}
+
+func Test_createRun(t *testing.T) {
+	tmpDir := t.TempDir()
+	taskDescFile := filepath.Join(tmpDir, "task-description.md")
+	emptyTaskDescFile := filepath.Join(tmpDir, "empty-task-description.md")
+	require.NoError(t, os.WriteFile(taskDescFile, []byte("task description from file"), 0600))
+	require.NoError(t, os.WriteFile(emptyTaskDescFile, []byte("  \n\n"), 0600))
+
+	sampleDateString := "2025-08-29T00:00:00Z"
+	sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
+	require.NoError(t, err)
+
+	createdJobSuccess := capi.Job{
+		ID:        "job123",
+		SessionID: "sess1",
+		Actor: &capi.JobActor{
+			ID:    1,
+			Login: "octocat",
+		},
+		CreatedAt: sampleDate,
+		UpdatedAt: sampleDate,
+	}
+	createdJobSuccessWithPR := capi.Job{
+		ID:        "job123",
+		SessionID: "sess1",
+		Actor: &capi.JobActor{
+			ID:    1,
+			Login: "octocat",
+		},
+		CreatedAt: sampleDate,
+		UpdatedAt: sampleDate,
+		PullRequest: &capi.JobPullRequest{
+			ID:     101,
+			Number: 42,
+		},
+	}
+
+	tests := []struct {
+		name             string
+		isTTY            bool
+		opts             *CreateOptions // input options (IO & BackOff set later)
+		capiStubs        func(*testing.T, *capi.CapiClientMock)
+		logRendererStubs func(*testing.T, *shared.LogRendererMock)
+		wantStdout       string
+		wantStdErr       string
+		wantErr          string
+		wantErrIs        error
+	}{
+		{
+			name:  "interactive, problem statement from arg",
+			isTTY: true,
+			opts: &CreateOptions{
+				BaseRepo:         func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil },
+				ProblemStatement: "task description from arg",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "task description from arg", problemStatement)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name: "non-interactive, problem statement from arg",
+			opts: &CreateOptions{
+				BaseRepo:         func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil },
+				ProblemStatement: "task description from arg",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "task description from arg", problemStatement)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name:  "interactive, problem statement from file",
+			isTTY: true,
+			opts: &CreateOptions{
+				BaseRepo:             func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil },
+				ProblemStatement:     "",
+				ProblemStatementFile: taskDescFile,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "task description from file", problemStatement)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name: "non-interactive, problem statement loaded from file",
+			opts: &CreateOptions{
+				BaseRepo:             func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil },
+				ProblemStatement:     "",
+				ProblemStatementFile: taskDescFile,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "task description from file", problemStatement)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name:  "interactive, problem statement from prompt/editor",
+			isTTY: true,
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				Prompter: &prompter.PrompterMock{
+					MarkdownEditorFunc: func(prompt, defaultValue string, blankAllowed bool) (string, error) {
+						require.Equal(t, "Enter the task description", prompt)
+						return "From editor", nil
+					},
+				},
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "From editor", problemStatement)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name:  "interactive, empty task description from editor returns error",
+			isTTY: true,
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				Prompter: &prompter.PrompterMock{
+					MarkdownEditorFunc: func(prompt, defaultValue string, blankAllowed bool) (string, error) {
+						return "   ", nil
+					},
+				},
+			},
+			wantErr: "a task description is required",
+		},
+		{
+			name: "missing repo returns error",
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return nil, nil
+				}},
+			wantErr: "a repository is required; re-run in a repository or supply one with --repo owner/name",
+		},
+		{
+			name: "problem statement loaded from arg non-interactively doesn't prompt or return error",
+			opts: &CreateOptions{
+				BaseRepo:         func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil },
+				ProblemStatement: "task description",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "task description", problemStatement)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name: "base branch included in create payload",
+			opts: &CreateOptions{
+				BaseRepo:         func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil },
+				ProblemStatement: "Do the thing",
+				BaseBranch:       "feature",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "Do the thing", problemStatement)
+					require.Equal(t, "feature", baseBranch)
+					return &createdJobSuccess, nil
+				}
+				m.GetJobFunc = func(ctx context.Context, owner, repo, jobID string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "job123", jobID)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name: "create task API failure returns error",
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				ProblemStatement: "Do the thing",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "Do the thing", problemStatement)
+					require.Equal(t, "", baseBranch)
+					return nil, errors.New("some API error")
+				}
+			},
+			wantErr: "some API error",
+		},
+		{
+			name: "get job API failure surfaces error",
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				ProblemStatement: "Do the thing",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "Do the thing", problemStatement)
+					require.Equal(t, "", baseBranch)
+					return &createdJobSuccess, nil
+				}
+				m.GetJobFunc = func(ctx context.Context, owner, repo, jobID string) (*capi.Job, error) {
+					return nil, errors.New("some error")
+				}
+			},
+			wantStdErr: "some error\n",
+			wantStdout: "job job123 queued. View progress: https://github.com/copilot/agents\n",
+		},
+		{
+			name: "success with immediate PR",
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				ProblemStatement: "Do the thing",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "Do the thing", problemStatement)
+					require.Equal(t, "", baseBranch)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name: "success with delayed PR after polling",
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				ProblemStatement: "Do the thing",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "Do the thing", problemStatement)
+					require.Equal(t, "", baseBranch)
+					return &createdJobSuccess, nil
+				}
+				m.GetJobFunc = func(ctx context.Context, owner, repo, jobID string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "job123", jobID)
+					return &createdJobSuccessWithPR, nil
+				}
+			},
+			wantStdout: "https://github.com/OWNER/REPO/pull/42/agent-sessions/sess1\n",
+		},
+		{
+			name: "fallback after polling timeout returns link to global agents page",
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				ProblemStatement: "Do the thing",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "Do the thing", problemStatement)
+					require.Equal(t, "", baseBranch)
+					return &createdJobSuccess, nil
+				}
+
+				count := 0
+				m.GetJobFunc = func(ctx context.Context, owner, repo, jobID string) (*capi.Job, error) {
+					if count++; count > 4 {
+						require.FailNow(t, "too many get calls")
+					}
+					return &createdJobSuccess, nil
+				}
+			},
+			wantStdout: "job job123 queued. View progress: https://github.com/copilot/agents\n",
+		},
+		{
+			name: "success with follow logs and delayed PR after polling",
+			opts: &CreateOptions{
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				ProblemStatement: "Do the thing",
+				Follow:           true,
+				Sleep:            func(d time.Duration) {},
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.CreateJobFunc = func(ctx context.Context, owner, repo, problemStatement, baseBranch string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "Do the thing", problemStatement)
+					require.Equal(t, "", baseBranch)
+					return &createdJobSuccess, nil
+				}
+				m.GetJobFunc = func(ctx context.Context, owner, repo, jobID string) (*capi.Job, error) {
+					require.Equal(t, "OWNER", owner)
+					require.Equal(t, "REPO", repo)
+					require.Equal(t, "job123", jobID)
+					return &createdJobSuccessWithPR, nil
+				}
+
+				var count int
+				m.GetSessionLogsFunc = func(_ context.Context, id string) ([]byte, error) {
+					assert.Equal(t, "sess1", id)
+
+					count++
+					require.Less(t, count, 3, "too many calls to fetch logs")
+					if count == 1 {
+						return []byte("<raw-logs-one>"), nil
+					}
+					return []byte("<raw-logs-two>"), nil
+				}
+			},
+			logRendererStubs: func(t *testing.T, m *shared.LogRendererMock) {
+				m.FollowFunc = func(fetcher func() ([]byte, error), w io.Writer, ios *iostreams.IOStreams) error {
+					raw, err := fetcher()
+					require.NoError(t, err)
+					w.Write([]byte("(rendered:) " + string(raw) + "\n"))
+
+					raw, err = fetcher()
+					require.NoError(t, err)
+					w.Write([]byte("(rendered:) " + string(raw) + "\n"))
+					return nil
+				}
+			},
+			wantStdout: heredoc.Doc(`
+				(rendered:) <raw-logs-one>
+				(rendered:) <raw-logs-two>
+			`),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			capiClientMock := &capi.CapiClientMock{}
+			if tt.capiStubs != nil {
+				tt.capiStubs(t, capiClientMock)
+			}
+
+			ios, _, stdout, stderr := iostreams.Test()
+			if tt.isTTY {
+				ios.SetStdinTTY(true)
+				ios.SetStderrTTY(true)
+				ios.SetStdoutTTY(true)
+			}
+
+			tt.opts.IO = ios
+			tt.opts.CapiClient = func() (capi.CapiClient, error) {
+				return capiClientMock, nil
+			}
+
+			// fast backoff
+			tt.opts.BackOff = backoff.WithMaxRetries(&backoff.ZeroBackOff{}, 3)
+
+			logRenderer := &shared.LogRendererMock{}
+			if tt.logRendererStubs != nil {
+				tt.logRendererStubs(t, logRenderer)
+			}
+			tt.opts.LogRenderer = func() shared.LogRenderer {
+				return logRenderer
+			}
+
+			err := createRun(tt.opts)
+			if tt.wantErrIs != nil {
+				require.ErrorIs(t, err, tt.wantErrIs)
+			}
+			if tt.wantErr != "" {
+				require.Error(t, err)
+				require.Equal(t, tt.wantErr, err.Error())
+			} else if tt.wantErrIs == nil {
+				require.NoError(t, err)
+			}
+
+			require.Equal(t, tt.wantStdout, stdout.String())
+			require.Equal(t, tt.wantStdErr, stderr.String())
+		})
+	}
+}
diff --git a/pkg/cmd/agent-task/list/list_test.go b/pkg/cmd/agent-task/list/list_test.go
new file mode 100644
index 000000000..747650283
--- /dev/null
+++ b/pkg/cmd/agent-task/list/list_test.go
@@ -0,0 +1,334 @@
+package list
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"testing"
+	"time"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/api"
+	"github.com/cli/cli/v2/internal/browser"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/google/shlex"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewCmdList(t *testing.T) {
+	tests := []struct {
+		name     string
+		args     string
+		wantOpts ListOptions
+		wantErr  string
+	}{
+		{
+			name: "no arguments",
+			wantOpts: ListOptions{
+				Limit: defaultLimit,
+			},
+		},
+		{
+			name: "custom limit",
+			args: "--limit 15",
+			wantOpts: ListOptions{
+				Limit: 15,
+			},
+		},
+		{
+			name:    "invalid limit",
+			args:    "--limit 0",
+			wantErr: "invalid limit: 0",
+		},
+		{
+			name:    "negative limit",
+			args:    "--limit -5",
+			wantErr: "invalid limit: -5",
+		},
+		{
+			name: "web flag",
+			args: "--web",
+			wantOpts: ListOptions{
+				Limit: defaultLimit,
+				Web:   true,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ios, _, _, _ := iostreams.Test()
+			f := &cmdutil.Factory{
+				IOStreams: ios,
+			}
+
+			var gotOpts *ListOptions
+			cmd := NewCmdList(f, func(opts *ListOptions) error { gotOpts = opts; return nil })
+
+			argv, err := shlex.Split(tt.args)
+			require.NoError(t, err)
+			cmd.SetArgs(argv)
+
+			cmd.SetIn(&bytes.Buffer{})
+			cmd.SetOut(io.Discard)
+			cmd.SetErr(io.Discard)
+
+			_, err = cmd.ExecuteC()
+			if tt.wantErr != "" {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.wantErr)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantOpts.Limit, gotOpts.Limit)
+			assert.Equal(t, tt.wantOpts.Web, gotOpts.Web)
+		})
+	}
+}
+
+func Test_listRun(t *testing.T) {
+	sampleDate := time.Now().Add(-6 * time.Hour) // 6h ago
+	sampleDateString := sampleDate.Format(time.RFC3339)
+
+	tests := []struct {
+		name           string
+		tty            bool
+		capiStubs      func(*testing.T, *capi.CapiClientMock)
+		limit          int
+		web            bool
+		wantOut        string
+		wantErr        error
+		wantStderr     string
+		wantBrowserURL string
+	}{
+		{
+			name: "viewer-scoped no sessions",
+			tty:  true,
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListLatestSessionsForViewerFunc = func(ctx context.Context, limit int) ([]*capi.Session, error) {
+					return nil, nil
+				}
+			},
+			wantErr: cmdutil.NewNoResultsError("no agent tasks found"),
+		},
+		{
+			name:  "viewer-scoped respects --limit",
+			tty:   true,
+			limit: 999,
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListLatestSessionsForViewerFunc = func(ctx context.Context, limit int) ([]*capi.Session, error) {
+					assert.Equal(t, 999, limit)
+					return nil, nil
+				}
+			},
+			wantErr: cmdutil.NewNoResultsError("no agent tasks found"), // not important
+		},
+		{
+			name: "viewer-scoped single session (tty)",
+			tty:  true,
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListLatestSessionsForViewerFunc = func(ctx context.Context, limit int) ([]*capi.Session, error) {
+					return []*capi.Session{
+						{
+							ID:           "id1",
+							Name:         "s1",
+							State:        "completed",
+							CreatedAt:    sampleDate,
+							ResourceType: "pull",
+							PullRequest: &api.PullRequest{
+								Number: 101,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Showing 1 session
+
+				SESSION NAME  PULL REQUEST  REPO        SESSION STATE     CREATED
+				s1            #101          OWNER/REPO  Ready for review  about 6 hours ago
+			`),
+		},
+		{
+			name: "viewer-scoped single session (nontty)",
+			tty:  false,
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListLatestSessionsForViewerFunc = func(ctx context.Context, limit int) ([]*capi.Session, error) {
+					return []*capi.Session{
+						{
+							ID:           "id1",
+							Name:         "s1",
+							State:        "completed",
+							ResourceType: "pull",
+							CreatedAt:    sampleDate,
+							PullRequest: &api.PullRequest{
+								Number: 101,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+					}, nil
+				}
+			},
+			wantOut: "s1\t#101\tOWNER/REPO\tReady for review\t" + sampleDateString + "\n", // header omitted for non-tty
+		},
+		{
+			name: "viewer-scoped many sessions (tty)",
+			tty:  true,
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListLatestSessionsForViewerFunc = func(ctx context.Context, limit int) ([]*capi.Session, error) {
+					return []*capi.Session{
+						{
+							ID:           "id1",
+							Name:         "s1",
+							State:        "completed",
+							CreatedAt:    sampleDate,
+							ResourceType: "pull",
+							PullRequest: &api.PullRequest{
+								Number: 101,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+						{
+							ID:           "id2",
+							Name:         "s2",
+							State:        "failed",
+							CreatedAt:    sampleDate,
+							ResourceType: "pull",
+							PullRequest: &api.PullRequest{
+								Number: 102,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+						{
+							ID:           "id3",
+							Name:         "s3",
+							State:        "in_progress",
+							CreatedAt:    sampleDate,
+							ResourceType: "pull",
+							PullRequest: &api.PullRequest{
+								Number: 103,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+						{
+							ID:           "id4",
+							Name:         "s4",
+							State:        "queued",
+							CreatedAt:    sampleDate,
+							ResourceType: "pull",
+							PullRequest: &api.PullRequest{
+								Number: 104,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+						{
+							ID:           "id5",
+							Name:         "s5",
+							State:        "cancelled",
+							CreatedAt:    sampleDate,
+							ResourceType: "pull",
+							PullRequest: &api.PullRequest{
+								Number: 105,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+						{
+							ID:           "id6",
+							Name:         "s6",
+							State:        "mystery",
+							CreatedAt:    sampleDate,
+							ResourceType: "pull",
+							PullRequest: &api.PullRequest{
+								Number: 106,
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Showing 6 sessions
+
+				SESSION NAME  PULL REQUEST  REPO        SESSION STATE     CREATED
+				s1            #101          OWNER/REPO  Ready for review  about 6 hours ago
+				s2            #102          OWNER/REPO  Failed            about 6 hours ago
+				s3            #103          OWNER/REPO  In progress       about 6 hours ago
+				s4            #104          OWNER/REPO  Queued            about 6 hours ago
+				s5            #105          OWNER/REPO  Cancelled         about 6 hours ago
+				s6            #106          OWNER/REPO  mystery           about 6 hours ago
+			`),
+		},
+		{
+			name:           "web mode",
+			tty:            true,
+			web:            true,
+			wantOut:        "",
+			wantStderr:     "Opening https://github.com/copilot/agents in your browser.\n",
+			wantBrowserURL: "https://github.com/copilot/agents",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			capiClientMock := &capi.CapiClientMock{}
+			if tt.capiStubs != nil {
+				tt.capiStubs(t, capiClientMock)
+			}
+
+			ios, _, stdout, stderr := iostreams.Test()
+			ios.SetStdoutTTY(tt.tty)
+
+			var br *browser.Stub
+			if tt.web {
+				br = &browser.Stub{}
+			}
+
+			opts := &ListOptions{
+				IO:      ios,
+				Limit:   tt.limit,
+				Web:     tt.web,
+				Browser: br,
+				CapiClient: func() (capi.CapiClient, error) {
+					if tt.web {
+						require.FailNow(t, "CapiClient was called with --web")
+					}
+					return capiClientMock, nil
+				},
+			}
+
+			err := listRun(opts)
+			if tt.wantErr != nil {
+				assert.Error(t, err)
+				require.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				require.NoError(t, err)
+			}
+			got := stdout.String()
+			require.Equal(t, tt.wantOut, got)
+			require.Equal(t, tt.wantStderr, stderr.String())
+			if tt.web {
+				br.Verify(t, tt.wantBrowserURL)
+			}
+		})
+	}
+}
diff --git a/pkg/cmd/agent-task/shared/capi_test.go b/pkg/cmd/agent-task/shared/capi_test.go
new file mode 100644
index 000000000..205d881c8
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/capi_test.go
@@ -0,0 +1,76 @@
+package shared
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIsSession(t *testing.T) {
+	assert.True(t, IsSessionID("00000000-0000-0000-0000-000000000000"))
+	assert.True(t, IsSessionID("e2fa49d2-f164-4a56-ab99-498090b8fcdf"))
+	assert.True(t, IsSessionID("E2FA49D2-F164-4A56-AB99-498090B8FCDF"))
+
+	assert.False(t, IsSessionID(""))
+	assert.False(t, IsSessionID(" "))
+	assert.False(t, IsSessionID("\n"))
+	assert.False(t, IsSessionID("not-a-uuid"))
+	assert.False(t, IsSessionID("000000000000000000000000000000000000"))
+	assert.False(t, IsSessionID("00000000-0000-0000-0000-000000000000-extra"))
+}
+
+func TestParsePullRequestAgentSessionURL(t *testing.T) {
+	tests := []struct {
+		name          string
+		url           string
+		wantSessionID string
+		wantErr       bool
+	}{
+		{
+			name:          "valid",
+			url:           "https://github.com/OWNER/REPO/pull/123/agent-sessions/e2fa49d2-f164-4a56-ab99-498090b8fcdf",
+			wantSessionID: "e2fa49d2-f164-4a56-ab99-498090b8fcdf",
+		},
+		{
+			name:    "invalid session id",
+			url:     "https://github.com/OWNER/REPO/pull/123/agent-sessions/fff",
+			wantErr: true,
+		},
+		{
+			name:    "no session id, trailing slash",
+			url:     "https://github.com/OWNER/REPO/pull/123/agent-sessions/",
+			wantErr: true,
+		},
+		{
+			name:    "no session id",
+			url:     "https://github.com/OWNER/REPO/pull/123/agent-sessions",
+			wantErr: true,
+		},
+		{
+			name:    "invalid pr url",
+			url:     "https://github.com/OWNER/REPO/issues/123",
+			wantErr: true,
+		},
+		{
+			name:    "empty",
+			url:     "",
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			sessionID, err := ParseSessionIDFromURL(tt.url)
+
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.Zero(t, sessionID)
+				return
+			}
+
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantSessionID, sessionID)
+		})
+	}
+}
diff --git a/pkg/cmd/agent-task/shared/log_test.go b/pkg/cmd/agent-task/shared/log_test.go
new file mode 100644
index 000000000..07b562dc8
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/log_test.go
@@ -0,0 +1,95 @@
+package shared
+
+import (
+	"os"
+	"slices"
+	"strings"
+	"testing"
+
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestFollow(t *testing.T) {
+	tests := []struct {
+		name           string
+		log            string
+		wantStdoutFile string
+		wantStderrFile string
+	}{
+		{
+			name:           "sample log 1",
+			log:            "testdata/log-1-input.txt",
+			wantStdoutFile: "testdata/log-1-want.txt",
+		},
+		{
+			name:           "sample log 2",
+			log:            "testdata/log-2-input.txt",
+			wantStdoutFile: "testdata/log-2-want.txt",
+		},
+		{
+			name:           "sample log 3 (tolerant parse failures)",
+			log:            "testdata/log-3-synthetic-failures-input.txt",
+			wantStdoutFile: "testdata/log-3-synthetic-failures-want.txt",
+			wantStderrFile: "testdata/log-3-synthetic-failures-want-stderr.txt",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			raw, err := os.ReadFile(tt.log)
+			require.NoError(t, err)
+
+			// Normalize CRLF to LF to make the tests OS-agnostic.
+			raw = []byte(strings.ReplaceAll(string(raw), "\r\n", "\n"))
+
+			lines := slices.DeleteFunc(strings.Split(string(raw), "\n"), func(line string) bool {
+				return line == ""
+			})
+
+			var hits int
+			fetcher := func() ([]byte, error) {
+				hits++
+				if hits > len(lines) {
+					require.FailNow(t, "too many API calls")
+				}
+				return []byte(strings.Join(lines[0:hits], "\n\n")), nil
+			}
+
+			ios, _, stdout, stderr := iostreams.Test()
+
+			err = NewLogRenderer().Follow(fetcher, stdout, ios)
+			require.NoError(t, err)
+
+			// Handy note for updating the testdata files when they change:
+			// ext := filepath.Ext(tt.log)
+			// stripped := strings.TrimSuffix(tt.log, ext)
+			// stripped = strings.TrimSuffix(stripped, "-input")
+			// os.WriteFile(stripped+"-want"+ext, stdout.Bytes(), 0644)
+			// if tt.wantStderrFile != "" {
+			// 	os.WriteFile(stripped+"-want-stderr"+ext, stderr.Bytes(), 0644)
+			// }
+
+			wantStdout, err := os.ReadFile(tt.wantStdoutFile)
+			require.NoError(t, err)
+
+			// Normalize CRLF to LF to make the tests OS-agnostic.
+			wantStdout = []byte(strings.ReplaceAll(string(wantStdout), "\r\n", "\n"))
+
+			assert.Equal(t, string(wantStdout), stdout.String())
+
+			if tt.wantStderrFile != "" {
+				wantStderr, err := os.ReadFile(tt.wantStderrFile)
+				require.NoError(t, err)
+
+				// Normalize CRLF to LF to make the tests OS-agnostic.
+				wantStderr = []byte(strings.ReplaceAll(string(wantStderr), "\r\n", "\n"))
+
+				assert.Equal(t, string(wantStderr), stderr.String())
+			} else {
+				require.Empty(t, stderr, "expected no stderr output")
+			}
+		})
+	}
+}
diff --git a/pkg/cmd/agent-task/shared/testdata/log-1-input.txt b/pkg/cmd/agent-task/shared/testdata/log-1-input.txt
new file mode 100644
index 000000000..19fdb5dfd
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/testdata/log-1-input.txt
@@ -0,0 +1,64 @@
+data: {"id":"f85420df-3bbf-4ba9-bebd-7a17536cd61f","choices":[{"delta":{"content":"MCP server started successfully (version github-mcp-server/remote-1644693e4126d8c37794e77b2e09c6800709985e) with 39 tools - for the full output, see the verbose logs\n\n- download_workflow_run_artifact\n- get_code_scanning_alert\n- get_commit\n- get_file_contents\n- get_issue\n- get_issue_comments\n- get_job_logs\n- get_latest_release\n- get_pull_request\n- get_pull_request_comments\n- get_pull_request_diff\n- get_pull_request_files\n- get_pull_request_reviews\n- get_pull_request_status\n- get_release_by_tag\n- get_secret_scanning_alert\n- get_tag\n- get_workflow_run\n- get_workflow_run_logs\n- get_workflow_run_usage\n- list_branches\n- list_code_scanning_alerts\n- list_commits\n- list_issue_types\n- list_issues\n- list_pull_requests\n- list_releases\n- list_secret_scanning_alerts\n- list_sub_issues\n- list_tags\n- list_workflow_jobs\n- list_workflow_run_artifacts\n- list_workflow_runs\n- list_workflows\n- search_code\n- search_issues\n- search_pull_requests\n- search_repositories\n- search_users","role":"assistant","tool_calls":[{"function":{"name":"run_setup","arguments":"{\"name\":\"Start 'github-mcp-server' MCP server\"}"},"index":0,"id":"f85420df-3bbf-4ba9-bebd-7a17536cd61f"}]},"finish_reason":"tool_calls","index":0}],"created":1757413024352,"model":"","object":"chat.completion.chunk"}
+
+data: {"id":"c1ca2441-69a3-477a-bbc4-8544fd62630c","choices":[{"delta":{"content":"MCP server started successfully (version 0.0.35) with 21 tools - for the full output, see the verbose logs\n\n- browser_close\n- browser_resize\n- browser_console_messages\n- browser_handle_dialog\n- browser_evaluate\n- browser_file_upload\n- browser_fill_form\n- browser_install\n- browser_press_key\n- browser_type\n- browser_navigate\n- browser_navigate_back\n- browser_network_requests\n- browser_take_screenshot\n- browser_snapshot\n- browser_click\n- browser_drag\n- browser_hover\n- browser_select_option\n- browser_tabs\n- browser_wait_for","role":"assistant","tool_calls":[{"function":{"name":"run_setup","arguments":"{\"name\":\"Start 'playwright' MCP server\"}"},"index":0,"id":"c1ca2441-69a3-477a-bbc4-8544fd62630c"}]},"finish_reason":"tool_calls","index":0}],"created":1757413024832,"model":"","object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"I'll help you implement a shell script to print OS information. Let me start by exploring the repository structure to understand the codebase and then create a plan."},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo\"}","name":"view"},"id":"tooluse_HgRy3bczRe2bIxIR3T5deQ","type":"function","index":0}]},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && pwd && ls -la\",\"description\":\"Check current directory and list all files\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_p0U-8yIaTv2QbgGB4Z--hw","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413049,"id":"96e471e6-9a97-443c-b28d-a0d465406336","usage":{"completion_tokens":202,"prompt_tokens":18109,"prompt_tokens_details":{"cached_tokens":17458},"total_tokens":18311},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"96e471e6-9a97-443c-b28d-a0d465406336","created":1757413052,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo\"}","name":"view"},"id":"tooluse_HgRy3bczRe2bIxIR3T5deQ","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo b/home/runner/work/foo/foo\ncreate file mode 100644\nindex 0000000..0000000\n--- a/dev/null\n+++ b/home/runner/work/foo/foo\n@@ -1,0 +1,3 @@\n .git\n .github\n README.md\n"}}],"object":"chat.completion.chunk"}
+
+data: {"id":"96e471e6-9a97-443c-b28d-a0d465406336","created":1757413054,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && pwd && ls -la\",\"description\":\"Check current directory and list all files\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_p0U-8yIaTv2QbgGB4Z--hw","type":"function","index":0}],"content":"/home/runner/work/foo/foo\ntotal 20\ndrwxr-xr-x 4 runner docker 4096 Sep  9 10:17 .\ndrwxr-xr-x 3 runner docker 4096 Sep  9 10:16 ..\ndrwxr-xr-x 7 runner docker 4096 Sep  9 10:17 .git\ndrwxr-xr-x 4 runner docker 4096 Sep  9 10:17 .github\n-rw-r--r-- 1 runner docker    6 Sep  9 10:17 README.md\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Let me check the README and any existing workflow files to understand the repository structure better:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"view"},"id":"tooluse_HLjtr9JtSwCGSkyehEZn8A","type":"function","index":0}]},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/.github\"}","name":"view"},"id":"tooluse_GPlnrEqIQ226gXGgF934Dw","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413061,"id":"d8d65b32-47b5-47ad-b114-d1d9d626be6c","usage":{"completion_tokens":127,"prompt_tokens":18542,"prompt_tokens_details":{"cached_tokens":17458},"total_tokens":18669},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"d8d65b32-47b5-47ad-b114-d1d9d626be6c","created":1757413063,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/.github\"}","name":"view"},"id":"tooluse_GPlnrEqIQ226gXGgF934Dw","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/.github b/home/runner/work/foo/foo/.github\ncreate file mode 100644\nindex 0000000..0000000\n--- a/dev/null\n+++ b/home/runner/work/foo/foo/.github\n@@ -1,0 +1,4 @@\n PULL_REQUEST_TEMPLATE\n dependabot.yml\n pull_request_template.md\n workflows\n"}}],"object":"chat.completion.chunk"}
+
+data: {"id":"d8d65b32-47b5-47ad-b114-d1d9d626be6c","created":1757413065,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"view"},"id":"tooluse_HLjtr9JtSwCGSkyehEZn8A","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/README.md b/home/runner/work/foo/foo/README.md\nindex 0000000..0000000 100644\n--- a/home/runner/work/foo/foo/README.md\n+++ b/home/runner/work/foo/foo/README.md\n@@ -1,2 +1,2 @@\n # foo\n \n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/.github/workflows\"}","name":"view"},"id":"tooluse_m_FcAEbrTkWWktSyzBMt3w","type":"function","index":0}]},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && find . -name \\\"*.sh\\\" -o -name \\\"Makefile\\\" -o -name \\\"package.json\\\" -o -name \\\"*.yml\\\" -o -name \\\"*.yaml\\\"\",\"description\":\"Check if there are any existing scripts or build files\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_fOOqjjkiTDSDFUOvikXLxQ","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413072,"id":"0e4bb34a-fe12-462c-9be9-5daa0ff398d8","usage":{"completion_tokens":212,"prompt_tokens":18757,"prompt_tokens_details":{"cached_tokens":18301},"total_tokens":18969},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"0e4bb34a-fe12-462c-9be9-5daa0ff398d8","created":1757413074,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && find . -name \\\"*.sh\\\" -o -name \\\"Makefile\\\" -o -name \\\"package.json\\\" -o -name \\\"*.yml\\\" -o -name \\\"*.yaml\\\"\",\"description\":\"Check if there are any existing scripts or build files\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_fOOqjjkiTDSDFUOvikXLxQ","type":"function","index":0}],"content":"./.github/workflows/upload-artifact.yml\n./.github/workflows/manual-dispatch.yml\n./.github/workflows/cache.yml\n./.github/dependabot.yml\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"id":"0e4bb34a-fe12-462c-9be9-5daa0ff398d8","created":1757413075,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/.github/workflows\"}","name":"view"},"id":"tooluse_m_FcAEbrTkWWktSyzBMt3w","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/.github/workflows b/home/runner/work/foo/foo/.github/workflows\ncreate file mode 100644\nindex 0000000..0000000\n--- a/dev/null\n+++ b/home/runner/work/foo/foo/.github/workflows\n@@ -1,0 +1,3 @@\n cache.yml\n manual-dispatch.yml\n upload-artifact.yml\n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"thought\":\"Looking at the repository structure, I can see:\\n\\n1. This is a minimal repository with just a README.md and some GitHub workflows\\n2. There are no existing shell scripts or build infrastructure\\n3. The repository appears to be a simple project without any specific language or framework constraints\\n4. There are GitHub workflows but they seem to be for general CI/CD purposes\\n\\nFor the task \\\"add a shell script to print os information\\\", I need to:\\n1. Create a shell script that displays relevant OS information\\n2. Make it executable\\n3. Consider where to place it in the repository structure\\n4. Since this is a minimal repository, I should keep the script simple and place it in the root or a scripts directory\\n5. The script should display useful OS information like:\\n   - Operating system name and version\\n   - Kernel version\\n   - Architecture\\n   - CPU information\\n   - Memory information\\n   - Disk space\\n   - Uptime\\n\\nSince there's no existing test infrastructure and the instructions mention I can skip tests if there's no existing test infrastructure, I won't need to create tests.\\n\\nLet me plan the minimal changes:\\n1. Create a shell script (os_info.sh) in the root directory\\n2. Make it executable\\n3. Test it works correctly\\n4. Update README if appropriate\"}","name":"think"},"id":"tooluse_4z3OdYgpSra5wIGGLfmr6Q","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413085,"id":"e462d281-1592-4450-86af-87f173ff938a","usage":{"completion_tokens":331,"prompt_tokens":19094,"prompt_tokens_details":{"cached_tokens":18659},"total_tokens":19425},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"e462d281-1592-4450-86af-87f173ff938a","created":1757413086,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"thought\":\"Looking at the repository structure, I can see:\\n\\n1. This is a minimal repository with just a README.md and some GitHub workflows\\n2. There are no existing shell scripts or build infrastructure\\n3. The repository appears to be a simple project without any specific language or framework constraints\\n4. There are GitHub workflows but they seem to be for general CI/CD purposes\\n\\nFor the task \\\"add a shell script to print os information\\\", I need to:\\n1. Create a shell script that displays relevant OS information\\n2. Make it executable\\n3. Consider where to place it in the repository structure\\n4. Since this is a minimal repository, I should keep the script simple and place it in the root or a scripts directory\\n5. The script should display useful OS information like:\\n   - Operating system name and version\\n   - Kernel version\\n   - Architecture\\n   - CPU information\\n   - Memory information\\n   - Disk space\\n   - Uptime\\n\\nSince there's no existing test infrastructure and the instructions mention I can skip tests if there's no existing test infrastructure, I won't need to create tests.\\n\\nLet me plan the minimal changes:\\n1. Create a shell script (os_info.sh) in the root directory\\n2. Make it executable\\n3. Test it works correctly\\n4. Update README if appropriate\"}","name":"think"},"id":"tooluse_4z3OdYgpSra5wIGGLfmr6Q","type":"function","index":0}],"content":"Looking at the repository structure, I can see:\n\n1. This is a minimal repository with just a README.md and some GitHub workflows\n2. There are no existing shell scripts or build infrastructure\n3. The repository appears to be a simple project without any specific language or framework constraints\n4. There are GitHub workflows but they seem to be for general CI/CD purposes\n\nFor the task \"add a shell script to print os information\", I need to:\n1. Create a shell script that displays relevant OS information\n2. Make it executable\n3. Consider where to place it in the repository structure\n4. Since this is a minimal repository, I should keep the script simple and place it in the root or a scripts directory\n5. The script should display useful OS information like:\n   - Operating system name and version\n   - Kernel version\n   - Architecture\n   - CPU information\n   - Memory information\n   - Disk space\n   - Uptime\n\nSince there's no existing test infrastructure and the instructions mention I can skip tests if there's no existing test infrastructure, I won't need to create tests.\n\nLet me plan the minimal changes:\n1. Create a shell script (os_info.sh) in the root directory\n2. Make it executable\n3. Test it works correctly\n4. Update README if appropriate"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Now let me report my plan and create the shell script:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Initial plan: Add shell script to print OS information\",\"prDescription\":\"Adding a shell script to print OS information as requested.\\n\\n**Plan:**\\n- [ ] Create shell script `os_info.sh` in root directory that displays comprehensive OS information\\n- [ ] Include OS name/version, kernel, architecture, CPU, memory, disk space, and uptime information  \\n- [ ] Make script executable\\n- [ ] Test script functionality on current system\\n- [ ] Update README.md to document the new script\\n- [ ] Verify all changes work as expected\"}","name":"report_progress"},"id":"tooluse_70Lb3coESd-wlmpX6RwxPQ","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413094,"id":"05b5de29-2408-46f6-8c9c-7c8d07952a52","usage":{"completion_tokens":197,"prompt_tokens":19439,"prompt_tokens_details":{"cached_tokens":18959},"total_tokens":19636},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"05b5de29-2408-46f6-8c9c-7c8d07952a52","created":1757413098,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Initial plan: Add shell script to print OS information\",\"prDescription\":\"Adding a shell script to print OS information as requested.\\n\\n**Plan:**\\n- [ ] Create shell script `os_info.sh` in root directory that displays comprehensive OS information\\n- [ ] Include OS name/version, kernel, architecture, CPU, memory, disk space, and uptime information  \\n- [ ] Make script executable\\n- [ ] Test script functionality on current system\\n- [ ] Update README.md to document the new script\\n- [ ] Verify all changes work as expected\"}","name":"report_progress"},"id":"tooluse_70Lb3coESd-wlmpX6RwxPQ","type":"function","index":0}],"content":"$ git push -v origin copilot/add-os-information-shell-script-3\nPushing to https://github.com/gh-babakks/foo\nTo https://github.com/gh-babakks/foo\n = [up to date]      copilot/add-os-information-shell-script-3 -> copilot/add-os-information-shell-script-3\nupdating local tracking ref 'refs/remotes/origin/copilot/add-os-information-shell-script-3'\nEverything up-to-date\n\n\n\nProgress reported successfully."}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Now let me create the shell script with comprehensive OS information:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"file_text\":\"#!/bin/bash\\n\\n# OS Information Script\\n# This script displays comprehensive operating system information\\n\\necho \\\"==================================================\\\"\\necho \\\"         OPERATING SYSTEM INFORMATION\\\"\\necho \\\"==================================================\\\"\\necho\\n\\n# OS Name and Version\\necho \\\"📋 Operating System:\\\"\\nif command -v lsb_release &> /dev/null; then\\n    lsb_release -d | cut -d: -f2 | sed 's/^\\\\s*//'\\nelif [ -f /etc/os-release ]; then\\n    grep PRETTY_NAME /etc/os-release | cut -d= -f2 | sed 's/\\\"//g'\\nelif [ -f /etc/redhat-release ]; then\\n    cat /etc/redhat-release\\nelse\\n    uname -s\\nfi\\necho\\n\\n# Kernel Information\\necho \\\"🔧 Kernel Information:\\\"\\necho \\\"  Version: $(uname -r)\\\"\\necho \\\"  Architecture: $(uname -m)\\\"\\necho\\n\\n# CPU Information\\necho \\\"💻 CPU Information:\\\"\\nif [ -f /proc/cpuinfo ]; then\\n    cpu_model=$(grep \\\"model name\\\" /proc/cpuinfo | head -1 | cut -d: -f2 | sed 's/^\\\\s*//')\\n    cpu_cores=$(grep -c \\\"processor\\\" /proc/cpuinfo)\\n    echo \\\"  Model: $cpu_model\\\"\\n    echo \\\"  Cores: $cpu_cores\\\"\\nelse\\n    echo \\\"  Architecture: $(uname -m)\\\"\\nfi\\necho\\n\\n# Memory Information\\necho \\\"💾 Memory Information:\\\"\\nif [ -f /proc/meminfo ]; then\\n    total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')\\n    available_mem=$(grep MemAvailable /proc/meminfo | awk '{print $2}')\\n    if [ -n \\\"$total_mem\\\" ]; then\\n        total_gb=$((total_mem / 1024 / 1024))\\n        available_gb=$((available_mem / 1024 / 1024))\\n        echo \\\"  Total: ${total_gb}GB\\\"\\n        echo \\\"  Available: ${available_gb}GB\\\"\\n    fi\\nelse\\n    echo \\\"  Memory information not available\\\"\\nfi\\necho\\n\\n# Disk Space Information\\necho \\\"💽 Disk Space Information:\\\"\\ndf -h / 2>/dev/null | awk 'NR==2 {print \\\"  Root filesystem: \\\" $2 \\\" total, \\\" $3 \\\" used, \\\" $4 \\\" available (\\\" $5 \\\" used)\\\"}' || echo \\\"  Disk information not available\\\"\\necho\\n\\n# System Uptime\\necho \\\"⏰ System Uptime:\\\"\\nif command -v uptime &> /dev/null; then\\n    uptime -p 2>/dev/null || uptime | cut -d, -f1 | sed 's/.*up //'\\nelse\\n    echo \\\"  Uptime information not available\\\"\\nfi\\necho\\n\\n# Current Date and Time\\necho \\\"📅 Current Date and Time:\\\"\\ndate\\necho\\n\\necho \\\"==================================================\\\"\",\"path\":\"/home/runner/work/foo/foo/os_info.sh\"}","name":"create"},"id":"tooluse_1bYfGs6DTV6XMP6BZO7FUg","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413111,"id":"0d3a07d1-6edf-4838-9e26-82cbadbf314e","usage":{"completion_tokens":825,"prompt_tokens":19783,"prompt_tokens_details":{"cached_tokens":19414},"total_tokens":20608},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"0d3a07d1-6edf-4838-9e26-82cbadbf314e","created":1757413113,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"file_text\":\"#!/bin/bash\\n\\n# OS Information Script\\n# This script displays comprehensive operating system information\\n\\necho \\\"==================================================\\\"\\necho \\\"         OPERATING SYSTEM INFORMATION\\\"\\necho \\\"==================================================\\\"\\necho\\n\\n# OS Name and Version\\necho \\\"📋 Operating System:\\\"\\nif command -v lsb_release &> /dev/null; then\\n    lsb_release -d | cut -d: -f2 | sed 's/^\\\\s*//'\\nelif [ -f /etc/os-release ]; then\\n    grep PRETTY_NAME /etc/os-release | cut -d= -f2 | sed 's/\\\"//g'\\nelif [ -f /etc/redhat-release ]; then\\n    cat /etc/redhat-release\\nelse\\n    uname -s\\nfi\\necho\\n\\n# Kernel Information\\necho \\\"🔧 Kernel Information:\\\"\\necho \\\"  Version: $(uname -r)\\\"\\necho \\\"  Architecture: $(uname -m)\\\"\\necho\\n\\n# CPU Information\\necho \\\"💻 CPU Information:\\\"\\nif [ -f /proc/cpuinfo ]; then\\n    cpu_model=$(grep \\\"model name\\\" /proc/cpuinfo | head -1 | cut -d: -f2 | sed 's/^\\\\s*//')\\n    cpu_cores=$(grep -c \\\"processor\\\" /proc/cpuinfo)\\n    echo \\\"  Model: $cpu_model\\\"\\n    echo \\\"  Cores: $cpu_cores\\\"\\nelse\\n    echo \\\"  Architecture: $(uname -m)\\\"\\nfi\\necho\\n\\n# Memory Information\\necho \\\"💾 Memory Information:\\\"\\nif [ -f /proc/meminfo ]; then\\n    total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')\\n    available_mem=$(grep MemAvailable /proc/meminfo | awk '{print $2}')\\n    if [ -n \\\"$total_mem\\\" ]; then\\n        total_gb=$((total_mem / 1024 / 1024))\\n        available_gb=$((available_mem / 1024 / 1024))\\n        echo \\\"  Total: ${total_gb}GB\\\"\\n        echo \\\"  Available: ${available_gb}GB\\\"\\n    fi\\nelse\\n    echo \\\"  Memory information not available\\\"\\nfi\\necho\\n\\n# Disk Space Information\\necho \\\"💽 Disk Space Information:\\\"\\ndf -h / 2>/dev/null | awk 'NR==2 {print \\\"  Root filesystem: \\\" $2 \\\" total, \\\" $3 \\\" used, \\\" $4 \\\" available (\\\" $5 \\\" used)\\\"}' || echo \\\"  Disk information not available\\\"\\necho\\n\\n# System Uptime\\necho \\\"⏰ System Uptime:\\\"\\nif command -v uptime &> /dev/null; then\\n    uptime -p 2>/dev/null || uptime | cut -d, -f1 | sed 's/.*up //'\\nelse\\n    echo \\\"  Uptime information not available\\\"\\nfi\\necho\\n\\n# Current Date and Time\\necho \\\"📅 Current Date and Time:\\\"\\ndate\\necho\\n\\necho \\\"==================================================\\\"\",\"path\":\"/home/runner/work/foo/foo/os_info.sh\"}","name":"create"},"id":"tooluse_1bYfGs6DTV6XMP6BZO7FUg","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/os_info.sh b/home/runner/work/foo/foo/os_info.sh\ncreate file mode 100644\nindex 0000000..0000000\n--- a/dev/null\n+++ b/home/runner/work/foo/foo/os_info.sh\n@@ -1,0 +1,77 @@\n+#!/bin/bash\n+\n+# OS Information Script\n+# This script displays comprehensive operating system information\n+\n+echo \"==================================================\"\n+echo \"         OPERATING SYSTEM INFORMATION\"\n+echo \"==================================================\"\n+echo\n+\n+# OS Name and Version\n+echo \"📋 Operating System:\"\n+if command -v lsb_release &> /dev/null; then\n+    lsb_release -d | cut -d: -f2 | sed 's/^\\s*//'\n+elif [ -f /etc/os-release ]; then\n+    grep PRETTY_NAME /etc/os-release | cut -d= -f2 | sed 's/\"//g'\n+elif [ -f /etc/redhat-release ]; then\n+    cat /etc/redhat-release\n+else\n+    uname -s\n+fi\n+echo\n+\n+# Kernel Information\n+echo \"🔧 Kernel Information:\"\n+echo \"  Version: $(uname -r)\"\n+echo \"  Architecture: $(uname -m)\"\n+echo\n+\n+# CPU Information\n+echo \"💻 CPU Information:\"\n+if [ -f /proc/cpuinfo ]; then\n+    cpu_model=$(grep \"model name\" /proc/cpuinfo | head -1 | cut -d: -f2 | sed 's/^\\s*//')\n+    cpu_cores=$(grep -c \"processor\" /proc/cpuinfo)\n+    echo \"  Model: $cpu_model\"\n+    echo \"  Cores: $cpu_cores\"\n+else\n+    echo \"  Architecture: $(uname -m)\"\n+fi\n+echo\n+\n+# Memory Information\n+echo \"💾 Memory Information:\"\n+if [ -f /proc/meminfo ]; then\n+    total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')\n+    available_mem=$(grep MemAvailable /proc/meminfo | awk '{print $2}')\n+    if [ -n \"$total_mem\" ]; then\n+        total_gb=$((total_mem / 1024 / 1024))\n+        available_gb=$((available_mem / 1024 / 1024))\n+        echo \"  Total: ${total_gb}GB\"\n+        echo \"  Available: ${available_gb}GB\"\n+    fi\n+else\n+    echo \"  Memory information not available\"\n+fi\n+echo\n+\n+# Disk Space Information\n+echo \"💽 Disk Space Information:\"\n+df -h / 2>/dev/null | awk 'NR==2 {print \"  Root filesystem: \" $2 \" total, \" $3 \" used, \" $4 \" available (\" $5 \" used)\"}' || echo \"  Disk information not available\"\n+echo\n+\n+# System Uptime\n+echo \"⏰ System Uptime:\"\n+if command -v uptime &> /dev/null; then\n+    uptime -p 2>/dev/null || uptime | cut -d, -f1 | sed 's/.*up //'\n+else\n+    echo \"  Uptime information not available\"\n+fi\n+echo\n+\n+# Current Date and Time\n+echo \"📅 Current Date and Time:\"\n+date\n+echo\n+\n+echo \"==================================================\"\n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Now let me make the script executable and test it:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && chmod +x os_info.sh && ./os_info.sh\",\"description\":\"Make script executable and test it\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_glSi0wzWSvWs0ceTDwh4JQ","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413120,"id":"127409e7-00c7-4682-a28f-f6e13c65dc78","usage":{"completion_tokens":146,"prompt_tokens":20643,"prompt_tokens_details":{"cached_tokens":19626},"total_tokens":20789},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"127409e7-00c7-4682-a28f-f6e13c65dc78","created":1757413122,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && chmod +x os_info.sh && ./os_info.sh\",\"description\":\"Make script executable and test it\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_glSi0wzWSvWs0ceTDwh4JQ","type":"function","index":0}],"content":"==================================================\n         OPERATING SYSTEM INFORMATION\n==================================================\n\n📋 Operating System:\nUbuntu 24.04.3 LTS\n\n🔧 Kernel Information:\n  Version: 6.11.0-1018-azure\n  Architecture: x86_64\n\n💻 CPU Information:\n  Model: AMD EPYC 7763 64-Core Processor\n  Cores: 4\n\n💾 Memory Information:\n  Total: 15GB\n  Available: 14GB\n\n💽 Disk Space Information:\n  Root filesystem: 72G total, 46G used, 27G available (64% used)\n\n⏰ System Uptime:\nup 2 minutes\n\n📅 Current Date and Time:\nTue Sep  9 10:18:42 UTC 2025\n\n==================================================\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Great! The script works perfectly. Now let me update the README to document the new script:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"new_str\":\"# foo\\n\\n## OS Information Script\\n\\nThis repository contains a shell script to display comprehensive operating system information.\\n\\n### Usage\\n\\n```bash\\n./os_info.sh\\n```\\n\\nThe script displays:\\n- Operating system name and version\\n- Kernel version and architecture  \\n- CPU model and core count\\n- Memory usage information\\n- Disk space usage\\n- System uptime\\n- Current date and time\\n\\n### Requirements\\n\\n- Bash shell\\n- Standard Unix/Linux utilities (works on most Linux distributions)\",\"old_str\":\"# foo\",\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"str_replace"},"id":"tooluse_xmZ_Tz5ySNCQtfhYBEkhYQ","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413130,"id":"24ccb760-8b7e-4067-b59f-73a3d6894bb9","usage":{"completion_tokens":230,"prompt_tokens":21004,"prompt_tokens_details":{"cached_tokens":20597},"total_tokens":21234},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"24ccb760-8b7e-4067-b59f-73a3d6894bb9","created":1757413132,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"new_str\":\"# foo\\n\\n## OS Information Script\\n\\nThis repository contains a shell script to display comprehensive operating system information.\\n\\n### Usage\\n\\n```bash\\n./os_info.sh\\n```\\n\\nThe script displays:\\n- Operating system name and version\\n- Kernel version and architecture  \\n- CPU model and core count\\n- Memory usage information\\n- Disk space usage\\n- System uptime\\n- Current date and time\\n\\n### Requirements\\n\\n- Bash shell\\n- Standard Unix/Linux utilities (works on most Linux distributions)\",\"old_str\":\"# foo\",\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"str_replace"},"id":"tooluse_xmZ_Tz5ySNCQtfhYBEkhYQ","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/README.md b/home/runner/work/foo/foo/README.md\nindex 0000000..0000000 100644\n--- a/home/runner/work/foo/foo/README.md\n+++ b/home/runner/work/foo/foo/README.md\n@@ -1,2 +1,26 @@\n # foo\n \n+## OS Information Script\n+\n+This repository contains a shell script to display comprehensive operating system information.\n+\n+### Usage\n+\n+```bash\n+./os_info.sh\n+```\n+\n+The script displays:\n+- Operating system name and version\n+- Kernel version and architecture  \n+- CPU model and core count\n+- Memory usage information\n+- Disk space usage\n+- System uptime\n+- Current date and time\n+\n+### Requirements\n+\n+- Bash shell\n+- Standard Unix/Linux utilities (works on most Linux distributions)\n+\n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Let me verify the changes look good:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"view"},"id":"tooluse_dkzXcB1VSxCFAxRW6gWN4A","type":"function","index":0}]},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && ls -la os_info.sh && echo \\\"---\\\" && git status\",\"description\":\"Check file permissions and final status\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_nYQtQ-HZQeuL58w3q3DaXA","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413139,"id":"bdc3491a-a50e-4f31-b3c8-fc909d0b4322","usage":{"completion_tokens":191,"prompt_tokens":21264,"prompt_tokens_details":{"cached_tokens":20779},"total_tokens":21455},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"bdc3491a-a50e-4f31-b3c8-fc909d0b4322","created":1757413142,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && ls -la os_info.sh && echo \\\"---\\\" && git status\",\"description\":\"Check file permissions and final status\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_nYQtQ-HZQeuL58w3q3DaXA","type":"function","index":0}],"content":"-rwxr-xr-x 1 runner docker 2139 Sep  9 10:18 os_info.sh\n---\nOn branch copilot/add-os-information-shell-script-3\nYour branch is up to date with 'origin/copilot/add-os-information-shell-script-3'.\n\nChanges not staged for commit:\n  (use \"git add <file>...\" to update what will be committed)\n  (use \"git restore <file>...\" to discard changes in working directory)\n\tmodified:   README.md\n\nUntracked files:\n  (use \"git add <file>...\" to include in what will be committed)\n\tos_info.sh\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"id":"bdc3491a-a50e-4f31-b3c8-fc909d0b4322","created":1757413143,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"view"},"id":"tooluse_dkzXcB1VSxCFAxRW6gWN4A","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/README.md b/home/runner/work/foo/foo/README.md\nindex 0000000..0000000 100644\n--- a/home/runner/work/foo/foo/README.md\n+++ b/home/runner/work/foo/foo/README.md\n@@ -1,26 +1,26 @@\n # foo\n \n ## OS Information Script\n \n This repository contains a shell script to display comprehensive operating system information.\n \n ### Usage\n \n ```bash\n ./os_info.sh\n ```\n \n The script displays:\n - Operating system name and version\n - Kernel version and architecture  \n - CPU model and core count\n - Memory usage information\n - Disk space usage\n - System uptime\n - Current date and time\n \n ### Requirements\n \n - Bash shell\n - Standard Unix/Linux utilities (works on most Linux distributions)\n \n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Perfect! Let me test the script one more time to make sure everything is working correctly and commit the changes:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && echo \\\"Testing script execution:\\\" && ./os_info.sh | head -10\",\"description\":\"Final test of the script\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_ndAHpcXkSTm2_CAMXhE0qw","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413149,"id":"ca7380c9-bb6c-4375-8a8f-4c4199af2bfb","usage":{"completion_tokens":158,"prompt_tokens":21864,"prompt_tokens_details":{"cached_tokens":21224},"total_tokens":22022},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"ca7380c9-bb6c-4375-8a8f-4c4199af2bfb","created":1757413152,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && echo \\\"Testing script execution:\\\" && ./os_info.sh | head -10\",\"description\":\"Final test of the script\",\"sessionId\":\"main\"}","name":"bash"},"id":"tooluse_ndAHpcXkSTm2_CAMXhE0qw","type":"function","index":0}],"content":"Testing script execution:\n==================================================\n         OPERATING SYSTEM INFORMATION\n==================================================\n\n📋 Operating System:\nUbuntu 24.04.3 LTS\n\n🔧 Kernel Information:\n  Version: 6.11.0-1018-azure\n  Architecture: x86_64\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Excellent! Now let me commit all the changes:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Add os_info.sh shell script to display OS information\",\"prDescription\":\"Adding a shell script to print OS information as requested.\\n\\n**Plan:**\\n- [x] Create shell script `os_info.sh` in root directory that displays comprehensive OS information\\n- [x] Include OS name/version, kernel, architecture, CPU, memory, disk space, and uptime information  \\n- [x] Make script executable\\n- [x] Test script functionality on current system\\n- [x] Update README.md to document the new script\\n- [x] Verify all changes work as expected\\n\\n**Changes Made:**\\n- Created `os_info.sh` - executable shell script that displays comprehensive OS information including:\\n  - Operating system name and version (with fallbacks for different distros)\\n  - Kernel version and architecture\\n  - CPU model and core count\\n  - Memory usage (total and available)\\n  - Disk space usage for root filesystem\\n  - System uptime\\n  - Current date and time\\n- Updated README.md with usage instructions and script documentation\\n- Script includes proper error handling and fallbacks for different systems\"}","name":"report_progress"},"id":"tooluse_484Da3MSRnGoeHdag1y3yA","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413161,"id":"2237873a-8d78-4c98-a75d-e37c077723f1","usage":{"completion_tokens":321,"prompt_tokens":22111,"prompt_tokens_details":{"cached_tokens":21445},"total_tokens":22432},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"2237873a-8d78-4c98-a75d-e37c077723f1","created":1757413165,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Add os_info.sh shell script to display OS information\",\"prDescription\":\"Adding a shell script to print OS information as requested.\\n\\n**Plan:**\\n- [x] Create shell script `os_info.sh` in root directory that displays comprehensive OS information\\n- [x] Include OS name/version, kernel, architecture, CPU, memory, disk space, and uptime information  \\n- [x] Make script executable\\n- [x] Test script functionality on current system\\n- [x] Update README.md to document the new script\\n- [x] Verify all changes work as expected\\n\\n**Changes Made:**\\n- Created `os_info.sh` - executable shell script that displays comprehensive OS information including:\\n  - Operating system name and version (with fallbacks for different distros)\\n  - Kernel version and architecture\\n  - CPU model and core count\\n  - Memory usage (total and available)\\n  - Disk space usage for root filesystem\\n  - System uptime\\n  - Current date and time\\n- Updated README.md with usage instructions and script documentation\\n- Script includes proper error handling and fallbacks for different systems\"}","name":"report_progress"},"id":"tooluse_484Da3MSRnGoeHdag1y3yA","type":"function","index":0}],"content":"$ git add . -v\nadd 'README.md'\nadd 'os_info.sh'\n\n$ git commit -m Add os_info.sh shell script to display OS information\n\nCo-authored-by: babakks <36728931+babakks@users.noreply.github.com>\n[copilot/add-os-information-shell-script-3 4bbc3e5] Add os_info.sh shell script to display OS information\n 2 files changed, 101 insertions(+)\n create mode 100755 os_info.sh\n\n$ git push -v origin copilot/add-os-information-shell-script-3\nPushing to https://github.com/gh-babakks/foo\nPOST git-receive-pack (1732 bytes)\nTo https://github.com/gh-babakks/foo\n   4e83749..4bbc3e5  copilot/add-os-information-shell-script-3 -> copilot/add-os-information-shell-script-3\nupdating local tracking ref 'refs/remotes/origin/copilot/add-os-information-shell-script-3'\n\n\n\nProgress reported successfully."}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Perfect! I have successfully implemented the shell script to print OS information. Let me run the CodeQL security checker to ensure there are no security vulnerabilities:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"codeql_checker"},"id":"tooluse_euKLZhSPTu-QqOjtapqgTw","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757413171,"id":"ef3f53a9-54fa-4c5b-a3ac-400638090a14","usage":{"completion_tokens":70,"prompt_tokens":22727,"prompt_tokens_details":{"cached_tokens":22012},"total_tokens":22797},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"id":"ef3f53a9-54fa-4c5b-a3ac-400638090a14","created":1757413174,"model":"Claude Sonnet 4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{}","name":"codeql_checker"},"id":"tooluse_euKLZhSPTu-QqOjtapqgTw","type":"function","index":0}],"content":"<info>Analysis completed successfully for .</info>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Excellent! The security analysis shows no vulnerabilities. \n\n## Summary\n\nI have successfully completed the task by adding a comprehensive shell script to print OS information. Here's what was implemented:\n\n### ✅ **Completed Changes:**\n\n1. **Created `os_info.sh`** - A robust, executable shell script that displays:\n   - Operating system name and version (with smart detection for different Linux distros)\n   - Kernel version and architecture\n   - CPU model and core count\n   - Memory usage (total and available)\n   - Disk space usage for root filesystem\n   - System uptime\n   - Current date and time\n\n2. **Updated README.md** - Added comprehensive documentation including:\n   - Usage instructions\n   - Description of information displayed\n   - System requirements\n\n3. **Made the script executable** and thoroughly tested its functionality\n\n### 🔧 **Key Features:**\n\n- **Cross-platform compatibility**: Works on various Linux distributions with fallback mechanisms\n- **Error handling**: Gracefully handles missing commands or files\n- **User-friendly output**: Clean formatting with emoji icons and organized sections\n- **Comprehensive information**: Covers all major system information categories\n- **Executable permissions**: Properly set for immediate use\n\nThe script is ready to use and provides a complete overview of the operating system information as requested. It has been tested and works correctly on the current Ubuntu system, displaying all relevant OS details in an organized and readable format."},"finish_reason":"stop"}],"created":1757413184,"id":"f62df9bf-33fe-41a1-a76b-1142bba8ae01","usage":{"completion_tokens":319,"prompt_tokens":22814,"prompt_tokens_details":{"cached_tokens":22422},"total_tokens":23133},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"<pr_title>\nAdd shell script to display comprehensive OS information\n</pr_title>\n\n<pr_description>\nThis PR adds a new shell script `os_info.sh` that displays comprehensive operating system information in a user-friendly format.\n\n## What's Added\n\nThe script provides detailed system information including:\n- **Operating System**: Name and version with fallback support for different Linux distributions\n- **Kernel Information**: Version and architecture\n- **CPU Details**: Model name and core count\n- **Memory Usage**: Total and available memory in GB\n- **Disk Space**: Root filesystem usage statistics\n- **System Uptime**: How long the system has been running\n- **Current Date/Time**: Timestamp of when the script was executed\n\n## Features\n\n- **Cross-platform compatibility**: Works on most Linux distributions with appropriate fallbacks\n- **Error handling**: Gracefully handles missing system information\n- **Clean output**: Formatted with emojis and clear sections for easy reading\n- **Executable**: Script is properly set with execute permissions\n\n## Usage\n\n```bash\n./os_info.sh\n```\n\nExample output:\n```\n==================================================\n         OPERATING SYSTEM INFORMATION\n==================================================\n\n📋 Operating System:\nUbuntu 24.04.3 LTS\n\n🔧 Kernel Information:\n  Version: 6.11.0-1018-azure\n  Architecture: x86_64\n\n💻 CPU Information:\n  Model: AMD EPYC 7763 64-Core Processor\n  Cores: 4\n```\n\nThe README has been updated with usage instructions and requirements documentation.\n</pr_description>"},"finish_reason":"stop"}],"created":1757413195,"id":"0a4ae0d9-60d3-422f-a2c2-c78d583ee142","usage":{"completion_tokens":365,"prompt_tokens":23069,"prompt_tokens_details":{"cached_tokens":17458},"total_tokens":23434},"model":"Claude Sonnet 4","object":"chat.completion.chunk"}
+
diff --git a/pkg/cmd/agent-task/shared/testdata/log-1-want.txt b/pkg/cmd/agent-task/shared/testdata/log-1-want.txt
new file mode 100644
index 000000000..6ba206dda
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/testdata/log-1-want.txt
@@ -0,0 +1,425 @@
+Start 'github-mcp-server' MCP server
+Start 'playwright' MCP server
+
+I'll help you implement a shell script to print OS information. Let me start
+  by exploring the repository structure to understand the codebase and then   
+  create a plan.
+
+View repository
+.git                                                                     
+     .github                                                                  
+     README.md
+
+Bash: Check current directory and list all files
+$ cd /home/runner/work/foo/foo && pwd && ls -la                           
+    /home/runner/work/foo/foo                                                 
+    total 20                                                                  
+    drwxr-xr-x 4 runner docker 4096 Sep  9 10:17 .                            
+    drwxr-xr-x 3 runner docker 4096 Sep  9 10:16 ..                           
+    drwxr-xr-x 7 runner docker 4096 Sep  9 10:17 .git                         
+    drwxr-xr-x 4 runner docker 4096 Sep  9 10:17 .github                      
+    -rw-r--r-- 1 runner docker    6 Sep  9 10:17 README.md                    
+    <exited with exit code 0>
+
+
+Let me check the README and any existing workflow files to understand the   
+  repository structure better:
+
+View .github
+PULL_REQUEST_TEMPLATE                                                    
+     dependabot.yml                                                           
+     pull_request_template.md                                                 
+     workflows
+
+View README.md
+
+# foo
+
+Bash: Check if there are any existing scripts or build files
+$ cd /home/runner/work/foo/foo && find . -name "*.sh" -o -name "Makefile" -
+  o -name "package.json" -o -name "*.yml" -o -name "*.yaml"                   
+    ./.github/workflows/upload-artifact.yml                                   
+    ./.github/workflows/manual-dispatch.yml                                   
+    ./.github/workflows/cache.yml                                             
+    ./.github/dependabot.yml                                                  
+    <exited with exit code 0>
+
+View .github/workflows
+cache.yml                                                                
+     manual-dispatch.yml                                                      
+     upload-artifact.yml
+
+Thought
+
+Looking at the repository structure, I can see:                             
+                                                                              
+  1. This is a minimal repository with just a README.md and some GitHub       
+  workflows                                                                   
+  2. There are no existing shell scripts or build infrastructure              
+  3. The repository appears to be a simple project without any specific       
+  language or framework constraints                                           
+  4. There are GitHub workflows but they seem to be for general CI/CD purposes
+                                                                              
+  For the task "add a shell script to print os information", I need to:       
+                                                                              
+  1. Create a shell script that displays relevant OS information              
+  2. Make it executable                                                       
+  3. Consider where to place it in the repository structure                   
+  4. Since this is a minimal repository, I should keep the script simple and  
+  place it in the root or a scripts directory                                 
+  5. The script should display useful OS information like:                    
+      • Operating system name and version                                     
+      • Kernel version                                                        
+      • Architecture                                                          
+      • CPU information                                                       
+      • Memory information                                                    
+      • Disk space                                                            
+      • Uptime                                                                
+                                                                              
+                                                                              
+  Since there's no existing test infrastructure and the instructions mention I
+  can skip tests if there's no existing test infrastructure, I won't need to  
+  create tests.                                                               
+                                                                              
+  Let me plan the minimal changes:                                            
+                                                                              
+  1. Create a shell script (os_info.sh) in the root directory                 
+  2. Make it executable                                                       
+  3. Test it works correctly                                                  
+  4. Update README if appropriate
+
+
+Now let me report my plan and create the shell script:
+
+Progress update: Initial plan: Add shell script to print OS information
+
+Adding a shell script to print OS information as requested.                 
+                                                                              
+  **Plan:**                                                                   
+                                                                              
+  [ ] Create shell script os_info.sh in root directory that displays          
+  comprehensive OS information                                                
+  [ ] Include OS name/version, kernel, architecture, CPU, memory, disk space, 
+  and uptime information                                                      
+  [ ] Make script executable                                                  
+  [ ] Test script functionality on current system                             
+  [ ] Update README.md to document the new script                             
+  [ ] Verify all changes work as expected
+
+
+Now let me create the shell script with comprehensive OS information:
+
+Create: os_info.sh
+#!/bin/bash                                                               
+                                                                              
+    # OS Information Script                                                   
+    # This script displays comprehensive operating system information         
+                                                                              
+    echo "=================================================="                 
+    echo "         OPERATING SYSTEM INFORMATION"                              
+    echo "=================================================="                 
+    echo                                                                      
+                                                                              
+    # OS Name and Version                                                     
+    echo "📋 Operating System:"                                               
+    if command -v lsb_release &> /dev/null; then                              
+        lsb_release -d | cut -d: -f2 | sed 's/^\s*//'                         
+    elif [ -f /etc/os-release ]; then                                         
+        grep PRETTY_NAME /etc/os-release | cut -d= -f2 | sed 's/"//g'         
+    elif [ -f /etc/redhat-release ]; then                                     
+        cat /etc/redhat-release                                               
+    else                                                                      
+        uname -s                                                              
+    fi                                                                        
+    echo                                                                      
+                                                                              
+    # Kernel Information                                                      
+    echo "🔧 Kernel Information:"                                             
+    echo "  Version: $(uname -r)"                                             
+    echo "  Architecture: $(uname -m)"                                        
+    echo                                                                      
+                                                                              
+    # CPU Information                                                         
+    echo "💻 CPU Information:"                                                
+    if [ -f /proc/cpuinfo ]; then                                             
+        cpu_model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | 
+  sed 's/^\s*//')                                                             
+        cpu_cores=$(grep -c "processor" /proc/cpuinfo)                        
+        echo "  Model: $cpu_model"                                            
+        echo "  Cores: $cpu_cores"                                            
+    else                                                                      
+        echo "  Architecture: $(uname -m)"                                    
+    fi                                                                        
+    echo                                                                      
+                                                                              
+    # Memory Information                                                      
+    echo "💾 Memory Information:"                                             
+    if [ -f /proc/meminfo ]; then                                             
+        total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')           
+        available_mem=$(grep MemAvailable /proc/meminfo | awk '{print $2}')   
+        if [ -n "$total_mem" ]; then                                          
+            total_gb=$((total_mem / 1024 / 1024))                             
+            available_gb=$((available_mem / 1024 / 1024))                     
+            echo "  Total: ${total_gb}GB"                                     
+            echo "  Available: ${available_gb}GB"                             
+        fi                                                                    
+    else                                                                      
+        echo "  Memory information not available"                             
+    fi                                                                        
+    echo                                                                      
+                                                                              
+    # Disk Space Information                                                  
+    echo "💽 Disk Space Information:"                                         
+    df -h / 2>/dev/null | awk 'NR==2 {print "  Root filesystem: " $2 " total, 
+  " $3 " used, " $4 " available (" $5 " used)"}' || echo "  Disk information  
+  not available"                                                              
+    echo                                                                      
+                                                                              
+    # System Uptime                                                           
+    echo "⏰ System Uptime:"                                                  
+    if command -v uptime &> /dev/null; then                                   
+        uptime -p 2>/dev/null || uptime | cut -d, -f1 | sed 's/.*up //'       
+    else                                                                      
+        echo "  Uptime information not available"                             
+    fi                                                                        
+    echo                                                                      
+                                                                              
+    # Current Date and Time                                                   
+    echo "📅 Current Date and Time:"                                          
+    date                                                                      
+    echo                                                                      
+                                                                              
+    echo "=================================================="
+
+
+Now let me make the script executable and test it:
+
+Bash: Make script executable and test it
+$ cd /home/runner/work/foo/foo && chmod +x os_info.sh && ./os_info.sh     
+    ==================================================                        
+             OPERATING SYSTEM INFORMATION                                     
+    ==================================================                        
+                                                                              
+    📋 Operating System:                                                      
+    Ubuntu 24.04.3 LTS                                                        
+                                                                              
+    🔧 Kernel Information:                                                    
+      Version: 6.11.0-1018-azure                                              
+      Architecture: x86_64                                                    
+                                                                              
+    💻 CPU Information:                                                       
+      Model: AMD EPYC 7763 64-Core Processor                                  
+      Cores: 4                                                                
+                                                                              
+    💾 Memory Information:                                                    
+      Total: 15GB                                                             
+      Available: 14GB                                                         
+                                                                              
+    💽 Disk Space Information:                                                
+      Root filesystem: 72G total, 46G used, 27G available (64% used)          
+                                                                              
+    ⏰ System Uptime:                                                         
+    up 2 minutes                                                              
+                                                                              
+    📅 Current Date and Time:                                                 
+    Tue Sep  9 10:18:42 UTC 2025                                              
+                                                                              
+    ==================================================                        
+    <exited with exit code 0>
+
+
+Great! The script works perfectly. Now let me update the README to document 
+  the new script:
+
+Edit: README.md
+diff --git a/home/runner/work/foo/foo/README.md                           
+  b/home/runner/work/foo/foo/README.md                                        
+    index 0000000..0000000 100644                                             
+    --- a/home/runner/work/foo/foo/README.md                                  
+    +++ b/home/runner/work/foo/foo/README.md                                  
+    @@ -1,2 +1,26 @@                                                          
+     # foo                                                                    
+                                                                              
+    +## OS Information Script                                                 
+    +                                                                         
+    +This repository contains a shell script to display comprehensive         
+  operating system information.                                               
+    +                                                                         
+    +### Usage                                                                
+    +                                                                         
+    +```bash                                                                  
+    +./os_info.sh                                                             
+    +```                                                                      
+    +                                                                         
+    +The script displays:                                                     
+    +- Operating system name and version                                      
+    +- Kernel version and architecture                                        
+    +- CPU model and core count                                               
+    +- Memory usage information                                               
+    +- Disk space usage                                                       
+    +- System uptime                                                          
+    +- Current date and time                                                  
+    +                                                                         
+    +### Requirements                                                         
+    +                                                                         
+    +- Bash shell                                                             
+    +- Standard Unix/Linux utilities (works on most Linux distributions)      
+    +
+
+
+Let me verify the changes look good:
+
+Bash: Check file permissions and final status
+$ cd /home/runner/work/foo/foo && ls -la os_info.sh && echo "---" && git  
+  status                                                                      
+    -rwxr-xr-x 1 runner docker 2139 Sep  9 10:18 os_info.sh                   
+    ---                                                                       
+    On branch copilot/add-os-information-shell-script-3                       
+    Your branch is up to date with 'origin/copilot/add-os-information-shell-  
+  script-3'.                                                                  
+                                                                              
+    Changes not staged for commit:                                            
+      (use "git add <file>..." to update what will be committed)              
+      (use "git restore <file>..." to discard changes in working directory)   
+    	modified:   README.md                                                     
+                                                                              
+    Untracked files:                                                          
+      (use "git add <file>..." to include in what will be committed)          
+    	os_info.sh                                                                
+                                                                              
+    no changes added to commit (use "git add" and/or "git commit -a")         
+    <exited with exit code 0>
+
+View README.md
+
+# foo                                                                       
+                                                                              
+  ## OS Information Script                                                    
+                                                                              
+  This repository contains a shell script to display comprehensive operating  
+  system information.                                                         
+                                                                              
+  ### Usage                                                                   
+                                                                              
+    ./os_info.sh                                                              
+                                                                              
+  The script displays:                                                        
+                                                                              
+  • Operating system name and version                                         
+  • Kernel version and architecture                                           
+  • CPU model and core count                                                  
+  • Memory usage information                                                  
+  • Disk space usage                                                          
+  • System uptime                                                             
+  • Current date and time                                                     
+                                                                              
+  ### Requirements                                                            
+                                                                              
+  • Bash shell                                                                
+  • Standard Unix/Linux utilities (works on most Linux distributions)
+
+
+Perfect! Let me test the script one more time to make sure everything is    
+  working correctly and commit the changes:
+
+Bash: Final test of the script
+$ cd /home/runner/work/foo/foo && echo "Testing script execution:" && .   
+  /os_info.sh | head -10                                                      
+    Testing script execution:                                                 
+    ==================================================                        
+             OPERATING SYSTEM INFORMATION                                     
+    ==================================================                        
+                                                                              
+    📋 Operating System:                                                      
+    Ubuntu 24.04.3 LTS                                                        
+                                                                              
+    🔧 Kernel Information:                                                    
+      Version: 6.11.0-1018-azure                                              
+      Architecture: x86_64                                                    
+    <exited with exit code 0>
+
+
+Excellent! Now let me commit all the changes:
+
+Progress update: Add os_info.sh shell script to display OS information
+
+Adding a shell script to print OS information as requested.                 
+                                                                              
+  **Plan:**                                                                   
+                                                                              
+  [x] Create shell script os_info.sh in root directory that displays          
+  comprehensive OS information                                                
+  [x] Include OS name/version, kernel, architecture, CPU, memory, disk space, 
+  and uptime information                                                      
+  [x] Make script executable                                                  
+  [x] Test script functionality on current system                             
+  [x] Update README.md to document the new script                             
+  [x] Verify all changes work as expected                                     
+                                                                              
+  **Changes Made:**                                                           
+                                                                              
+  • Created os_info.sh - executable shell script that displays comprehensive  
+  OS information including:                                                   
+      • Operating system name and version (with fallbacks for different       
+      distros)                                                                
+      • Kernel version and architecture                                       
+      • CPU model and core count                                              
+      • Memory usage (total and available)                                    
+      • Disk space usage for root filesystem                                  
+      • System uptime                                                         
+      • Current date and time                                                 
+  • Updated README.md with usage instructions and script documentation        
+  • Script includes proper error handling and fallbacks for different systems
+
+
+Perfect! I have successfully implemented the shell script to print OS       
+  information. Let me run the CodeQL security checker to ensure there are no  
+  security vulnerabilities:
+
+Run CodeQL analysis
+
+Input:
+
+{}
+
+
+Excellent! The security analysis shows no vulnerabilities.                  
+                                                                              
+  ## Summary                                                                  
+                                                                              
+  I have successfully completed the task by adding a comprehensive shell      
+  script to print OS information. Here's what was implemented:                
+                                                                              
+  ### ✅ **Completed Changes:**                                               
+                                                                              
+  1. **Created **os_info.sh - A robust, executable shell script that displays:
+      • Operating system name and version (with smart detection for different 
+      Linux distros)                                                          
+      • Kernel version and architecture                                       
+      • CPU model and core count                                              
+      • Memory usage (total and available)                                    
+      • Disk space usage for root filesystem                                  
+      • System uptime                                                         
+      • Current date and time                                                 
+  2. **Updated README.md** - Added comprehensive documentation including:     
+      • Usage instructions                                                    
+      • Description of information displayed                                  
+      • System requirements                                                   
+  3. **Made the script executable** and thoroughly tested its functionality   
+                                                                              
+  ### 🔧 **Key Features:**                                                    
+                                                                              
+  • **Cross-platform compatibility**: Works on various Linux distributions    
+  with fallback mechanisms                                                    
+  • **Error handling**: Gracefully handles missing commands or files          
+  • **User-friendly output**: Clean formatting with emoji icons and organized 
+  sections                                                                    
+  • **Comprehensive information**: Covers all major system information        
+  categories                                                                  
+  • **Executable permissions**: Properly set for immediate use                
+                                                                              
+  The script is ready to use and provides a complete overview of the operating
+  system information as requested. It has been tested and works correctly on  
+  the current Ubuntu system, displaying all relevant OS details in an         
+  organized and readable format.
+
diff --git a/pkg/cmd/agent-task/shared/testdata/log-2-input.txt b/pkg/cmd/agent-task/shared/testdata/log-2-input.txt
new file mode 100644
index 000000000..4a77bbbac
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/testdata/log-2-input.txt
@@ -0,0 +1,62 @@
+data: {"id":"fe0942b2-814c-455e-a930-738b1c006e21","choices":[{"delta":{"content":"MCP server started successfully (version github-mcp-server/remote-84df50f6d2a32c75816994627b178853b0fdfc7b) with 39 tools - for the full output, see the verbose logs\n\n- download_workflow_run_artifact\n- get_code_scanning_alert\n- get_commit\n- get_file_contents\n- get_issue\n- get_issue_comments\n- get_job_logs\n- get_latest_release\n- get_pull_request\n- get_pull_request_comments\n- get_pull_request_diff\n- get_pull_request_files\n- get_pull_request_reviews\n- get_pull_request_status\n- get_release_by_tag\n- get_secret_scanning_alert\n- get_tag\n- get_workflow_run\n- get_workflow_run_logs\n- get_workflow_run_usage\n- list_branches\n- list_code_scanning_alerts\n- list_commits\n- list_issue_types\n- list_issues\n- list_pull_requests\n- list_releases\n- list_secret_scanning_alerts\n- list_sub_issues\n- list_tags\n- list_workflow_jobs\n- list_workflow_run_artifacts\n- list_workflow_runs\n- list_workflows\n- search_code\n- search_issues\n- search_pull_requests\n- search_repositories\n- search_users","role":"assistant","tool_calls":[{"function":{"name":"run_setup","arguments":"{\"name\":\"Start 'github-mcp-server' MCP server\"}"},"index":0,"id":"fe0942b2-814c-455e-a930-738b1c006e21"}]},"finish_reason":"tool_calls","index":0}],"created":1757597711559,"model":"","object":"chat.completion.chunk"}
+
+data: {"id":"505e4c5b-1b3b-4daa-9482-7d56ec62481c","choices":[{"delta":{"content":"MCP server started successfully (version 0.0.35) with 21 tools - for the full output, see the verbose logs\n\n- browser_close\n- browser_resize\n- browser_console_messages\n- browser_handle_dialog\n- browser_evaluate\n- browser_file_upload\n- browser_fill_form\n- browser_install\n- browser_press_key\n- browser_type\n- browser_navigate\n- browser_navigate_back\n- browser_network_requests\n- browser_take_screenshot\n- browser_snapshot\n- browser_click\n- browser_drag\n- browser_hover\n- browser_select_option\n- browser_tabs\n- browser_wait_for","role":"assistant","tool_calls":[{"function":{"name":"run_setup","arguments":"{\"name\":\"Start 'playwright' MCP server\"}"},"index":0,"id":"505e4c5b-1b3b-4daa-9482-7d56ec62481c"}]},"finish_reason":"tool_calls","index":0}],"created":1757597712436,"model":"","object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"I'll help you create a .yaml file with random fields in the repository. Let me start by exploring the repository structure to understand the codebase and then create a plan."},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo\"}","name":"view"},"id":"toolu_vrtx_01DtzuF1J9HqaLLmhjftXXuC","type":"function","index":0}]},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"command\":\"cd /home/runner/work/foo/foo && find . -type f -name \\\"*.md\\\" -o -name \\\"*.txt\\\" -o -name \\\"*.json\\\" -o -name \\\"*.yaml\\\" -o -name \\\"*.yml\\\" | head -20\",\"description\":\"Find documentation and config files to understand the repository\",\"sessionId\":\"main\",\"async\":false}","name":"bash"},"id":"toolu_vrtx_013ghLNCYV22MWdnhXD3Adfm","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597734,"id":"msg_vrtx_013f7aQC1EKiyT6zNgEwFA2A","usage":{"completion_tokens":249,"prompt_tokens":17583,"prompt_tokens_details":{"cached_tokens":12861},"total_tokens":17832},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_013f7aQC1EKiyT6zNgEwFA2A","created":1757597736,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"command\":\"cd /home/runner/work/foo/foo && find . -type f -name \\\"*.md\\\" -o -name \\\"*.txt\\\" -o -name \\\"*.json\\\" -o -name \\\"*.yaml\\\" -o -name \\\"*.yml\\\" | head -20\",\"description\":\"Find documentation and config files to understand the repository\",\"sessionId\":\"main\",\"async\":false}","name":"bash"},"id":"toolu_vrtx_013ghLNCYV22MWdnhXD3Adfm","type":"function","index":0}],"content":"./README.md\n./.github/workflows/upload-artifact.yml\n./.github/workflows/manual-dispatch.yml\n./.github/workflows/cache.yml\n./.github/dependabot.yml\n./.github/pull_request_template.md\n./.github/PULL_REQUEST_TEMPLATE/my-template.md\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_013f7aQC1EKiyT6zNgEwFA2A","created":1757597737,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo\"}","name":"view"},"id":"toolu_vrtx_01DtzuF1J9HqaLLmhjftXXuC","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo b/home/runner/work/foo/foo\ncreate file mode 100644\nindex 0000000..0000000\n--- a/dev/null\n+++ b/home/runner/work/foo/foo\n@@ -1,0 +1,3 @@\n .git\n .github\n README.md\n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"view"},"id":"toolu_vrtx_01GNSQBPRnAVwF5QbnJt1A22","type":"function","index":0}]},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && ls -la\",\"description\":\"List all files in the root directory\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_01RS1dKXeq1SP8cV3jkmsYbR","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597743,"id":"msg_vrtx_01UA6ZjdcvUKcnJUDJRk5wsv","usage":{"completion_tokens":170,"prompt_tokens":17986,"prompt_tokens_details":{"cached_tokens":16932},"total_tokens":18156},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01UA6ZjdcvUKcnJUDJRk5wsv","created":1757597746,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && ls -la\",\"description\":\"List all files in the root directory\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_01RS1dKXeq1SP8cV3jkmsYbR","type":"function","index":0}],"content":"total 20\ndrwxr-xr-x 4 runner runner 4096 Sep 11 13:35 .\ndrwxr-xr-x 3 runner runner 4096 Sep 11 13:34 ..\ndrwxrwxr-x 7 runner runner 4096 Sep 11 13:35 .git\ndrwxrwxr-x 4 runner runner 4096 Sep 11 13:35 .github\n-rw-rw-r-- 1 runner runner    6 Sep 11 13:35 README.md\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01UA6ZjdcvUKcnJUDJRk5wsv","created":1757597747,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/README.md\"}","name":"view"},"id":"toolu_vrtx_01GNSQBPRnAVwF5QbnJt1A22","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/README.md b/home/runner/work/foo/foo/README.md\nindex 0000000..0000000 100644\n--- a/home/runner/work/foo/foo/README.md\n+++ b/home/runner/work/foo/foo/README.md\n@@ -1,2 +1,2 @@\n # foo\n \n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && git --no-pager status\",\"description\":\"Check git status to understand current state\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_01VbQmGc46Ad34LghdzqTE8J","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597752,"id":"msg_vrtx_016S6MeX5RYpWcRFAV4Vna4p","usage":{"completion_tokens":127,"prompt_tokens":18376,"prompt_tokens_details":{"cached_tokens":17868},"total_tokens":18503},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_016S6MeX5RYpWcRFAV4Vna4p","created":1757597754,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && git --no-pager status\",\"description\":\"Check git status to understand current state\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_01VbQmGc46Ad34LghdzqTE8J","type":"function","index":0}],"content":"On branch copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25\nYour branch is up to date with 'origin/copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25'.\n\nnothing to commit, working tree clean\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"thought\":\"Based on my exploration of the repository, I can see that:\\n\\n1. This is a simple repository called \\\"foo\\\" with minimal content\\n2. It contains:\\n   - A basic README.md file with just \\\"# foo\\\"\\n   - A .github directory with some workflow files and templates\\n   - No existing source code, build system, or test infrastructure\\n   - Some existing YAML files in .github/workflows/ and .github/dependabot.yml\\n\\nThe problem statement is straightforward: \\\"create a .yaml file with random fields\\\". Since this is a minimal repository with no specific build/test infrastructure, I need to:\\n\\n1. Create a YAML file with some random fields\\n2. Since there's no test infrastructure, I don't need to add tests\\n3. Since there's no build system, there's nothing to build\\n4. This is a very simple change that just involves creating a new file\\n\\nLet me plan this out:\\n\\nPlan:\\n1. Create a YAML file with random fields in the root directory\\n2. Choose meaningful random fields that demonstrate various YAML data types (strings, numbers, booleans, arrays, objects)\\n3. Report progress with the changes\\n\\nThe change will be minimal - just adding one new file.\"}","name":"think"},"id":"toolu_vrtx_01Sia5awHBmbinDaVCfWt6i6","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597765,"id":"msg_vrtx_01FKTNJ9MshfGB82vC7TgAGk","usage":{"completion_tokens":317,"prompt_tokens":18609,"prompt_tokens_details":{"cached_tokens":18190},"total_tokens":18926},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01FKTNJ9MshfGB82vC7TgAGk","created":1757597766,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"thought\":\"Based on my exploration of the repository, I can see that:\\n\\n1. This is a simple repository called \\\"foo\\\" with minimal content\\n2. It contains:\\n   - A basic README.md file with just \\\"# foo\\\"\\n   - A .github directory with some workflow files and templates\\n   - No existing source code, build system, or test infrastructure\\n   - Some existing YAML files in .github/workflows/ and .github/dependabot.yml\\n\\nThe problem statement is straightforward: \\\"create a .yaml file with random fields\\\". Since this is a minimal repository with no specific build/test infrastructure, I need to:\\n\\n1. Create a YAML file with some random fields\\n2. Since there's no test infrastructure, I don't need to add tests\\n3. Since there's no build system, there's nothing to build\\n4. This is a very simple change that just involves creating a new file\\n\\nLet me plan this out:\\n\\nPlan:\\n1. Create a YAML file with random fields in the root directory\\n2. Choose meaningful random fields that demonstrate various YAML data types (strings, numbers, booleans, arrays, objects)\\n3. Report progress with the changes\\n\\nThe change will be minimal - just adding one new file.\"}","name":"think"},"id":"toolu_vrtx_01Sia5awHBmbinDaVCfWt6i6","type":"function","index":0}],"content":"Based on my exploration of the repository, I can see that:\n\n1. This is a simple repository called \"foo\" with minimal content\n2. It contains:\n   - A basic README.md file with just \"# foo\"\n   - A .github directory with some workflow files and templates\n   - No existing source code, build system, or test infrastructure\n   - Some existing YAML files in .github/workflows/ and .github/dependabot.yml\n\nThe problem statement is straightforward: \"create a .yaml file with random fields\". Since this is a minimal repository with no specific build/test infrastructure, I need to:\n\n1. Create a YAML file with some random fields\n2. Since there's no test infrastructure, I don't need to add tests\n3. Since there's no build system, there's nothing to build\n4. This is a very simple change that just involves creating a new file\n\nLet me plan this out:\n\nPlan:\n1. Create a YAML file with random fields in the root directory\n2. Choose meaningful random fields that demonstrate various YAML data types (strings, numbers, booleans, arrays, objects)\n3. Report progress with the changes\n\nThe change will be minimal - just adding one new file."}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Now I'll outline my plan and create the YAML file with random fields."},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Initial plan: Create YAML file with random fields\",\"prDescription\":\"Creating a .yaml file with random fields as requested.\\n\\n## Plan:\\n- [x] Explore repository structure and understand current state\\n- [ ] Create a .yaml file with various random fields demonstrating different YAML data types\\n- [ ] Verify the YAML file is valid\\n- [ ] Complete the implementation\\n\\n## Details:\\nThe repository is minimal with just a README and some GitHub workflow files. I'll create a new YAML file in the root directory with random fields including strings, numbers, booleans, arrays, and nested objects to demonstrate various YAML capabilities.\"}","name":"report_progress"},"id":"toolu_vrtx_019gXZoL1p4iQ2rWGYHVsxuF","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597775,"id":"msg_vrtx_01NaCn7ycmvQft1NL27xNjvW","usage":{"completion_tokens":218,"prompt_tokens":18941,"prompt_tokens_details":{"cached_tokens":18603},"total_tokens":19159},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01NaCn7ycmvQft1NL27xNjvW","created":1757597779,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Initial plan: Create YAML file with random fields\",\"prDescription\":\"Creating a .yaml file with random fields as requested.\\n\\n## Plan:\\n- [x] Explore repository structure and understand current state\\n- [ ] Create a .yaml file with various random fields demonstrating different YAML data types\\n- [ ] Verify the YAML file is valid\\n- [ ] Complete the implementation\\n\\n## Details:\\nThe repository is minimal with just a README and some GitHub workflow files. I'll create a new YAML file in the root directory with random fields including strings, numbers, booleans, arrays, and nested objects to demonstrate various YAML capabilities.\"}","name":"report_progress"},"id":"toolu_vrtx_019gXZoL1p4iQ2rWGYHVsxuF","type":"function","index":0}],"content":"$ git push -v origin copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25\nPushing to https://github.com/gh-babakks/foo\nTo https://github.com/gh-babakks/foo\n = [up to date]      copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25 -> copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25\nupdating local tracking ref 'refs/remotes/origin/copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25'\nEverything up-to-date\n\n\n\nProgress reported successfully."}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Now I'll create a YAML file with random fields that demonstrates various YAML data types and structures."},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/sample-data.yaml\",\"file_text\":\"# Sample YAML file with random fields demonstrating various data types\\napplication:\\n  name: \\\"RandomApp\\\"\\n  version: \\\"2.4.7\\\"\\n  environment: \\\"production\\\"\\n  debug_mode: false\\n  max_connections: 1000\\n  timeout_seconds: 30.5\\n\\ndatabase:\\n  host: \\\"db.example.com\\\"\\n  port: 5432\\n  username: \\\"app_user\\\"\\n  ssl_enabled: true\\n  connection_pool:\\n    min_size: 5\\n    max_size: 20\\n    idle_timeout: 300\\n\\nfeatures:\\n  - name: \\\"user_authentication\\\"\\n    enabled: true\\n    priority: 1\\n  - name: \\\"file_upload\\\"\\n    enabled: false\\n    priority: 3\\n  - name: \\\"real_time_notifications\\\"\\n    enabled: true\\n    priority: 2\\n\\nregions:\\n  - us-east-1\\n  - us-west-2\\n  - eu-central-1\\n  - ap-southeast-1\\n\\nmetadata:\\n  created_at: \\\"2023-09-15T10:30:00Z\\\"\\n  updated_at: \\\"2024-01-20T14:45:30Z\\\"\\n  tags:\\n    team: \\\"backend\\\"\\n    cost_center: \\\"engineering\\\"\\n    criticality: \\\"high\\\"\\n  \\napi:\\n  base_url: \\\"https://api.example.com/v1\\\"\\n  rate_limit: 1000\\n  retry_attempts: 3\\n  endpoints:\\n    users: \\\"/users\\\"\\n    orders: \\\"/orders\\\"\\n    analytics: \\\"/analytics\\\"\\n\\nmonitoring:\\n  health_check:\\n    path: \\\"/health\\\"\\n    interval: 60\\n    timeout: 10\\n  metrics:\\n    enabled: true\\n    export_format: \\\"prometheus\\\"\\n    retention_days: 30\\n\\nrandom_numbers:\\n  - 42\\n  - 3.14159\\n  - -17\\n  - 0.001\\n  - 9999999\\n\\nrandom_booleans:\\n  feature_flag_a: true\\n  feature_flag_b: false\\n  experimental_mode: true\\n  maintenance_mode: false\\n\\nnested_config:\\n  level1:\\n    level2:\\n      level3:\\n        deep_value: \\\"Found me!\\\"\\n        treasure: 12345\\n        coordinates:\\n          x: 10.5\\n          y: -20.3\\n          z: 100\"}","name":"create"},"id":"toolu_vrtx_012kkrKRu2UMXFSC9CmYohUC","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597793,"id":"msg_vrtx_01VLNz4KXQM3No1cEkdyixvC","usage":{"completion_tokens":676,"prompt_tokens":19370,"prompt_tokens_details":{"cached_tokens":18935},"total_tokens":20046},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01VLNz4KXQM3No1cEkdyixvC","created":1757597794,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/sample-data.yaml\",\"file_text\":\"# Sample YAML file with random fields demonstrating various data types\\napplication:\\n  name: \\\"RandomApp\\\"\\n  version: \\\"2.4.7\\\"\\n  environment: \\\"production\\\"\\n  debug_mode: false\\n  max_connections: 1000\\n  timeout_seconds: 30.5\\n\\ndatabase:\\n  host: \\\"db.example.com\\\"\\n  port: 5432\\n  username: \\\"app_user\\\"\\n  ssl_enabled: true\\n  connection_pool:\\n    min_size: 5\\n    max_size: 20\\n    idle_timeout: 300\\n\\nfeatures:\\n  - name: \\\"user_authentication\\\"\\n    enabled: true\\n    priority: 1\\n  - name: \\\"file_upload\\\"\\n    enabled: false\\n    priority: 3\\n  - name: \\\"real_time_notifications\\\"\\n    enabled: true\\n    priority: 2\\n\\nregions:\\n  - us-east-1\\n  - us-west-2\\n  - eu-central-1\\n  - ap-southeast-1\\n\\nmetadata:\\n  created_at: \\\"2023-09-15T10:30:00Z\\\"\\n  updated_at: \\\"2024-01-20T14:45:30Z\\\"\\n  tags:\\n    team: \\\"backend\\\"\\n    cost_center: \\\"engineering\\\"\\n    criticality: \\\"high\\\"\\n  \\napi:\\n  base_url: \\\"https://api.example.com/v1\\\"\\n  rate_limit: 1000\\n  retry_attempts: 3\\n  endpoints:\\n    users: \\\"/users\\\"\\n    orders: \\\"/orders\\\"\\n    analytics: \\\"/analytics\\\"\\n\\nmonitoring:\\n  health_check:\\n    path: \\\"/health\\\"\\n    interval: 60\\n    timeout: 10\\n  metrics:\\n    enabled: true\\n    export_format: \\\"prometheus\\\"\\n    retention_days: 30\\n\\nrandom_numbers:\\n  - 42\\n  - 3.14159\\n  - -17\\n  - 0.001\\n  - 9999999\\n\\nrandom_booleans:\\n  feature_flag_a: true\\n  feature_flag_b: false\\n  experimental_mode: true\\n  maintenance_mode: false\\n\\nnested_config:\\n  level1:\\n    level2:\\n      level3:\\n        deep_value: \\\"Found me!\\\"\\n        treasure: 12345\\n        coordinates:\\n          x: 10.5\\n          y: -20.3\\n          z: 100\"}","name":"create"},"id":"toolu_vrtx_012kkrKRu2UMXFSC9CmYohUC","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/sample-data.yaml b/home/runner/work/foo/foo/sample-data.yaml\ncreate file mode 100644\nindex 0000000..0000000\n--- a/dev/null\n+++ b/home/runner/work/foo/foo/sample-data.yaml\n@@ -1,0 +1,86 @@\n+# Sample YAML file with random fields demonstrating various data types\n+application:\n+  name: \"RandomApp\"\n+  version: \"2.4.7\"\n+  environment: \"production\"\n+  debug_mode: false\n+  max_connections: 1000\n+  timeout_seconds: 30.5\n+\n+database:\n+  host: \"db.example.com\"\n+  port: 5432\n+  username: \"app_user\"\n+  ssl_enabled: true\n+  connection_pool:\n+    min_size: 5\n+    max_size: 20\n+    idle_timeout: 300\n+\n+features:\n+  - name: \"user_authentication\"\n+    enabled: true\n+    priority: 1\n+  - name: \"file_upload\"\n+    enabled: false\n+    priority: 3\n+  - name: \"real_time_notifications\"\n+    enabled: true\n+    priority: 2\n+\n+regions:\n+  - us-east-1\n+  - us-west-2\n+  - eu-central-1\n+  - ap-southeast-1\n+\n+metadata:\n+  created_at: \"2023-09-15T10:30:00Z\"\n+  updated_at: \"2024-01-20T14:45:30Z\"\n+  tags:\n+    team: \"backend\"\n+    cost_center: \"engineering\"\n+    criticality: \"high\"\n+  \n+api:\n+  base_url: \"https://api.example.com/v1\"\n+  rate_limit: 1000\n+  retry_attempts: 3\n+  endpoints:\n+    users: \"/users\"\n+    orders: \"/orders\"\n+    analytics: \"/analytics\"\n+\n+monitoring:\n+  health_check:\n+    path: \"/health\"\n+    interval: 60\n+    timeout: 10\n+  metrics:\n+    enabled: true\n+    export_format: \"prometheus\"\n+    retention_days: 30\n+\n+random_numbers:\n+  - 42\n+  - 3.14159\n+  - -17\n+  - 0.001\n+  - 9999999\n+\n+random_booleans:\n+  feature_flag_a: true\n+  feature_flag_b: false\n+  experimental_mode: true\n+  maintenance_mode: false\n+\n+nested_config:\n+  level1:\n+    level2:\n+      level3:\n+        deep_value: \"Found me!\"\n+        treasure: 12345\n+        coordinates:\n+          x: 10.5\n+          y: -20.3\n+          z: 100\n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Now let me verify that the YAML file is valid by testing it with a YAML parser:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && python3 -c \\\"import yaml; yaml.safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!')\\\"\",\"description\":\"Validate the YAML file syntax\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_016xFTskoAmX34uHxg8m6rmq","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597802,"id":"msg_vrtx_01HRQQGQeEXaLKRAnpLcvWof","usage":{"completion_tokens":178,"prompt_tokens":20081,"prompt_tokens_details":{"cached_tokens":19365},"total_tokens":20259},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01HRQQGQeEXaLKRAnpLcvWof","created":1757597803,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && python3 -c \\\"import yaml; yaml.safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!')\\\"\",\"description\":\"Validate the YAML file syntax\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_016xFTskoAmX34uHxg8m6rmq","type":"function","index":0}],"content":"\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && python3 -c \\\"import yaml; data = yaml.safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!'); print(f'Loaded {len(data)} top-level keys: {list(data.keys())}')\\\"\",\"description\":\"Validate YAML and show structure\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_014g7kzLWv5BEZ4BWNbm5bzp","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597809,"id":"msg_vrtx_01SVMUS4b3UegHDN3yaqnBQd","usage":{"completion_tokens":184,"prompt_tokens":20280,"prompt_tokens_details":{"cached_tokens":20076},"total_tokens":20464},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01SVMUS4b3UegHDN3yaqnBQd","created":1757597811,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && python3 -c \\\"import yaml; data = yaml.safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!'); print(f'Loaded {len(data)} top-level keys: {list(data.keys())}')\\\"\",\"description\":\"Validate YAML and show structure\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_014g7kzLWv5BEZ4BWNbm5bzp","type":"function","index":0}],"content":"\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Let me try a different approach to verify the YAML:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && python3 -c \\\"\\nimport yaml\\ntry:\\n    with open('sample-data.yaml', 'r') as f:\\n        data = yaml.safe_load(f)\\n    print('YAML file is valid!')\\n    print(f'Loaded {len(data)} top-level keys: {list(data.keys())}')\\n    print(f'Application name: {data[\\\\\\\"application\\\\\\\"][\\\\\\\"name\\\\\\\"]}')\\n    print(f'Number of features: {len(data[\\\\\\\"features\\\\\\\"])}')\\nexcept Exception as e:\\n    print(f'Error: {e}')\\n\\\"\",\"description\":\"Detailed YAML validation with content verification\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_017EMDdJCqTE8tBwzM9PsYWb","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597818,"id":"msg_vrtx_01GbFRhuSh5bH1VPVEAMD8L9","usage":{"completion_tokens":275,"prompt_tokens":20485,"prompt_tokens_details":{"cached_tokens":20276},"total_tokens":20760},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01GbFRhuSh5bH1VPVEAMD8L9","created":1757597820,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && python3 -c \\\"\\nimport yaml\\ntry:\\n    with open('sample-data.yaml', 'r') as f:\\n        data = yaml.safe_load(f)\\n    print('YAML file is valid!')\\n    print(f'Loaded {len(data)} top-level keys: {list(data.keys())}')\\n    print(f'Application name: {data[\\\\\\\"application\\\\\\\"][\\\\\\\"name\\\\\\\"]}')\\n    print(f'Number of features: {len(data[\\\\\\\"features\\\\\\\"])}')\\nexcept Exception as e:\\n    print(f'Error: {e}')\\n\\\"\",\"description\":\"Detailed YAML validation with content verification\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_017EMDdJCqTE8tBwzM9PsYWb","type":"function","index":0}],"content":"YAML file is valid!\nLoaded 10 top-level keys: ['application', 'database', 'features', 'regions', 'metadata', 'api', 'monitoring', 'random_numbers', 'random_booleans', 'nested_config']\nApplication name: RandomApp\nNumber of features: 3\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Perfect! Let me also check the file contents and verify the current status:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/sample-data.yaml\",\"view_range\":[1,15]}","name":"view"},"id":"toolu_vrtx_019W2ACdpfZPudVbMfa3GwyY","type":"function","index":0}]},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && git --no-pager status\",\"description\":\"Check git status to see the new file\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_01CRcLvPHzuU6zEzP5C6D1op","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597828,"id":"msg_vrtx_016erDHLVsq6EnRF6RAQSi9p","usage":{"completion_tokens":217,"prompt_tokens":20858,"prompt_tokens_details":{"cached_tokens":20482},"total_tokens":21075},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_016erDHLVsq6EnRF6RAQSi9p","created":1757597830,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"async\":false,\"command\":\"cd /home/runner/work/foo/foo && git --no-pager status\",\"description\":\"Check git status to see the new file\",\"sessionId\":\"main\"}","name":"bash"},"id":"toolu_vrtx_01CRcLvPHzuU6zEzP5C6D1op","type":"function","index":0}],"content":"On branch copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25\nYour branch is up to date with 'origin/copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25'.\n\nUntracked files:\n  (use \"git add <file>...\" to include in what will be committed)\n\tsample-data.yaml\n\nnothing added to commit but untracked files present (use \"git add\" to track)\n<exited with exit code 0>"}}],"object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_016erDHLVsq6EnRF6RAQSi9p","created":1757597832,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"path\":\"/home/runner/work/foo/foo/sample-data.yaml\",\"view_range\":[1,15]}","name":"view"},"id":"toolu_vrtx_019W2ACdpfZPudVbMfa3GwyY","type":"function","index":0}],"content":"\ndiff --git a/home/runner/work/foo/foo/sample-data.yaml b/home/runner/work/foo/foo/sample-data.yaml\nindex 0000000..0000000 100644\n--- a/home/runner/work/foo/foo/sample-data.yaml\n+++ b/home/runner/work/foo/foo/sample-data.yaml\n@@ -1,15 +1,15 @@\n # Sample YAML file with random fields demonstrating various data types\n application:\n   name: \"RandomApp\"\n   version: \"2.4.7\"\n   environment: \"production\"\n   debug_mode: false\n   max_connections: 1000\n   timeout_seconds: 30.5\n \n database:\n   host: \"db.example.com\"\n   port: 5432\n   username: \"app_user\"\n   ssl_enabled: true\n   connection_pool:\n"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Excellent! Now let me report the completion of this task:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Add sample-data.yaml with random fields demonstrating various YAML data types\",\"prDescription\":\"Creating a .yaml file with random fields as requested.\\n\\n## Plan:\\n- [x] Explore repository structure and understand current state\\n- [x] Create a .yaml file with various random fields demonstrating different YAML data types\\n- [x] Verify the YAML file is valid\\n- [x] Complete the implementation\\n\\n## Implementation Details:\\nCreated `sample-data.yaml` with the following random fields and data types:\\n\\n### Data Types Demonstrated:\\n- **Strings**: Application names, URLs, usernames\\n- **Numbers**: Integers (ports, timeouts) and floats (version numbers, coordinates)\\n- **Booleans**: Feature flags and configuration options\\n- **Arrays**: Lists of regions, features, and random numbers\\n- **Objects**: Nested configuration structures\\n- **Mixed Arrays**: Arrays containing objects with multiple properties\\n\\n### Structure Includes:\\n- Application configuration (name, version, environment settings)\\n- Database connection details\\n- Feature flag configurations\\n- Regional deployment settings\\n- API endpoint mappings\\n- Monitoring configuration\\n- Deeply nested structures (3+ levels)\\n- Various random number examples\\n- Boolean flag examples\\n\\nThe YAML file has been validated and contains 10 top-level sections with diverse random data to demonstrate YAML capabilities.\"}","name":"report_progress"},"id":"toolu_vrtx_01X21uE7N4uwbUajy5Y1ou5n","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597844,"id":"msg_vrtx_012X2S9mbgHA4X7sGvGUr8oY","usage":{"completion_tokens":375,"prompt_tokens":21414,"prompt_tokens_details":{"cached_tokens":20856},"total_tokens":21789},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_012X2S9mbgHA4X7sGvGUr8oY","created":1757597849,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{\"commitMessage\":\"Add sample-data.yaml with random fields demonstrating various YAML data types\",\"prDescription\":\"Creating a .yaml file with random fields as requested.\\n\\n## Plan:\\n- [x] Explore repository structure and understand current state\\n- [x] Create a .yaml file with various random fields demonstrating different YAML data types\\n- [x] Verify the YAML file is valid\\n- [x] Complete the implementation\\n\\n## Implementation Details:\\nCreated `sample-data.yaml` with the following random fields and data types:\\n\\n### Data Types Demonstrated:\\n- **Strings**: Application names, URLs, usernames\\n- **Numbers**: Integers (ports, timeouts) and floats (version numbers, coordinates)\\n- **Booleans**: Feature flags and configuration options\\n- **Arrays**: Lists of regions, features, and random numbers\\n- **Objects**: Nested configuration structures\\n- **Mixed Arrays**: Arrays containing objects with multiple properties\\n\\n### Structure Includes:\\n- Application configuration (name, version, environment settings)\\n- Database connection details\\n- Feature flag configurations\\n- Regional deployment settings\\n- API endpoint mappings\\n- Monitoring configuration\\n- Deeply nested structures (3+ levels)\\n- Various random number examples\\n- Boolean flag examples\\n\\nThe YAML file has been validated and contains 10 top-level sections with diverse random data to demonstrate YAML capabilities.\"}","name":"report_progress"},"id":"toolu_vrtx_01X21uE7N4uwbUajy5Y1ou5n","type":"function","index":0}],"content":"$ git add . -v\nadd 'sample-data.yaml'\n\n$ git commit -m Add sample-data.yaml with random fields demonstrating various YAML data types\n\nCo-authored-by: babakks <36728931+babakks@users.noreply.github.com>\n[copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25 e9e29a7] Add sample-data.yaml with random fields demonstrating various YAML data types\n 1 file changed, 86 insertions(+)\n create mode 100644 sample-data.yaml\n\n$ git push -v origin copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25\nPushing to https://github.com/gh-babakks/foo\nPOST git-receive-pack (1462 bytes)\nTo https://github.com/gh-babakks/foo\n   830cc87..e9e29a7  copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25 -> copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25\nupdating local tracking ref 'refs/remotes/origin/copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25'\n\n\n\nProgress reported successfully."}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Perfect! I have successfully completed the task. Let me also run a security check to ensure there are no vulnerabilities in the changes:"},"finish_reason":"tool_calls"},{"delta":{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"codeql_checker"},"id":"toolu_vrtx_01CJZG4x83H1U2e1HkKMHEGi","type":"function","index":0}]},"finish_reason":"tool_calls"}],"created":1757597855,"id":"msg_vrtx_01DWsg7fF2gjbRZ4Gr7y6RjK","usage":{"completion_tokens":66,"prompt_tokens":22163,"prompt_tokens_details":{"cached_tokens":21249},"total_tokens":22229},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"id":"msg_vrtx_01DWsg7fF2gjbRZ4Gr7y6RjK","created":1757597860,"model":"claude-sonnet-4","choices":[{"finish_reason":"tool_calls","delta":{"tool_calls":[{"function":{"arguments":"{}","name":"codeql_checker"},"id":"toolu_vrtx_01CJZG4x83H1U2e1HkKMHEGi","type":"function","index":0}],"content":"<info>Analysis completed successfully for .</info>"}}],"object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"Great! The security check came back clean with no vulnerabilities detected.\n\n## Summary\n\nI have successfully implemented the requested changes to create a `.yaml` file with random fields. Here's what was accomplished:\n\n### ✅ **Task Completed Successfully**\n\n**Created**: `sample-data.yaml` - A comprehensive YAML file with diverse random fields\n\n### 📋 **What the YAML file contains:**\n\n1. **Application Configuration**: Name, version, environment settings, connection limits\n2. **Database Settings**: Host, port, credentials, connection pooling\n3. **Feature Management**: Array of features with enabled/disabled states and priorities\n4. **Regional Data**: List of AWS regions\n5. **Metadata**: Timestamps and organizational tags\n6. **API Configuration**: Base URLs, rate limits, endpoint mappings\n7. **Monitoring Setup**: Health checks and metrics configuration\n8. **Random Data Examples**: Numbers, booleans, and deeply nested structures\n\n### 🔧 **Data Types Demonstrated:**\n- Strings (quoted and unquoted)\n- Integers and floating-point numbers\n- Booleans (true/false)\n- Arrays/Lists\n- Objects/Maps\n- Nested structures (up to 4 levels deep)\n- Mixed data type arrays\n\n### ✅ **Validation:**\n- The YAML file syntax has been validated using Python's yaml parser\n- Successfully loads with 10 top-level keys\n- No security vulnerabilities detected\n- File properly committed and pushed to the repository\n\nThe implementation is minimal and focused, adding exactly what was requested - a single YAML file with random fields that demonstrates the full range of YAML data types and structures."},"finish_reason":"stop"}],"created":1757597872,"id":"msg_vrtx_015a4ikH7i829nZuNkAEiXPs","usage":{"completion_tokens":374,"prompt_tokens":22246,"prompt_tokens_details":{"cached_tokens":22162},"total_tokens":22620},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
+data: {"choices":[{"delta":{"role":"assistant","content":"<pr_title>\nAdd sample YAML file with diverse random fields and data types\n</pr_title>\n\n<pr_description>\nThis PR adds a comprehensive YAML file (`sample-data.yaml`) containing various random fields that demonstrate different YAML data types and structures.\n\n## What's Added\n\nThe new `sample-data.yaml` file includes:\n\n- **Application Configuration**: Name, version, environment settings with mixed data types\n- **Database Settings**: Connection details with nested objects and boolean flags\n- **Feature Management**: Array of objects with different properties and priorities\n- **Regional Data**: Simple string arrays for deployment regions\n- **API Configuration**: Nested endpoint mappings and rate limiting settings\n- **Monitoring Setup**: Health check parameters and metrics configuration\n- **Random Data Examples**: Arrays of numbers, boolean flags, and deeply nested structures\n\n## Data Types Demonstrated\n\nThe file showcases all major YAML data types:\n- Strings (quoted and unquoted)\n- Integers and floating-point numbers\n- Booleans (true/false)\n- Arrays (simple lists and complex object arrays)\n- Nested objects (up to 4 levels deep)\n- Mixed data structures\n\n## Example Structure\n\n```yaml\napplication:\n  name: \"RandomApp\"\n  version: \"2.4.7\"\n  debug_mode: false\n  max_connections: 1000\n\nfeatures:\n  - name: \"user_authentication\"\n    enabled: true\n    priority: 1\n  - name: \"file_upload\"\n    enabled: false\n    priority: 3\n\nnested_config:\n  level1:\n    level2:\n      level3:\n        deep_value: \"Found me!\"\n        coordinates:\n          x: 10.5\n          y: -20.3\n```\n\nThe YAML file has been validated for proper syntax and contains 86 lines of diverse random data across 10 top-level sections.\n</pr_description>"},"finish_reason":"stop"}],"created":1757597887,"id":"msg_vrtx_01So1PEUFXUoEDB1SeuHeDzK","usage":{"completion_tokens":440,"prompt_tokens":22501,"prompt_tokens_details":{"cached_tokens":16932},"total_tokens":22941},"model":"claude-sonnet-4","object":"chat.completion.chunk"}
+
diff --git a/pkg/cmd/agent-task/shared/testdata/log-2-want.txt b/pkg/cmd/agent-task/shared/testdata/log-2-want.txt
new file mode 100644
index 000000000..7665b4d51
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/testdata/log-2-want.txt
@@ -0,0 +1,371 @@
+Start 'github-mcp-server' MCP server
+Start 'playwright' MCP server
+
+I'll help you create a .yaml file with random fields in the repository. Let 
+  me start by exploring the repository structure to understand the codebase   
+  and then create a plan.
+
+Bash: Find documentation and config files to understand the repository
+$ cd /home/runner/work/foo/foo && find . -type f -name "*.md" -o -name "*.
+  txt" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" | head -20        
+    ./README.md                                                               
+    ./.github/workflows/upload-artifact.yml                                   
+    ./.github/workflows/manual-dispatch.yml                                   
+    ./.github/workflows/cache.yml                                             
+    ./.github/dependabot.yml                                                  
+    ./.github/pull_request_template.md                                        
+    ./.github/PULL_REQUEST_TEMPLATE/my-template.md                            
+    <exited with exit code 0>
+
+View repository
+.git                                                                     
+     .github                                                                  
+     README.md
+
+Bash: List all files in the root directory
+$ cd /home/runner/work/foo/foo && ls -la                                  
+    total 20                                                                  
+    drwxr-xr-x 4 runner runner 4096 Sep 11 13:35 .                            
+    drwxr-xr-x 3 runner runner 4096 Sep 11 13:34 ..                           
+    drwxrwxr-x 7 runner runner 4096 Sep 11 13:35 .git                         
+    drwxrwxr-x 4 runner runner 4096 Sep 11 13:35 .github                      
+    -rw-rw-r-- 1 runner runner    6 Sep 11 13:35 README.md                    
+    <exited with exit code 0>
+
+View README.md
+
+# foo
+
+Bash: Check git status to understand current state
+$ cd /home/runner/work/foo/foo && git --no-pager status                   
+    On branch copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25                
+    Your branch is up to date with 'origin/copilot/fix-5b1c2fcb-9248-492c-84fa-
+  04a3e0304d25'.                                                              
+                                                                              
+    nothing to commit, working tree clean                                     
+    <exited with exit code 0>
+
+Thought
+
+Based on my exploration of the repository, I can see that:                  
+                                                                              
+  1. This is a simple repository called "foo" with minimal content            
+  2. It contains:                                                             
+      • A basic README.md file with just "# foo"                              
+      • A .github directory with some workflow files and templates            
+      • No existing source code, build system, or test infrastructure         
+      • Some existing YAML files in .github/workflows/ and .github/dependabot.
+      yml                                                                     
+                                                                              
+                                                                              
+  The problem statement is straightforward: "create a .yaml file with random  
+  fields". Since this is a minimal repository with no specific build/test     
+  infrastructure, I need to:                                                  
+                                                                              
+  1. Create a YAML file with some random fields                               
+  2. Since there's no test infrastructure, I don't need to add tests          
+  3. Since there's no build system, there's nothing to build                  
+  4. This is a very simple change that just involves creating a new file      
+                                                                              
+  Let me plan this out:                                                       
+                                                                              
+  Plan:                                                                       
+                                                                              
+  1. Create a YAML file with random fields in the root directory              
+  2. Choose meaningful random fields that demonstrate various YAML data types 
+  (strings, numbers, booleans, arrays, objects)                               
+  3. Report progress with the changes                                         
+                                                                              
+  The change will be minimal - just adding one new file.
+
+
+Now I'll outline my plan and create the YAML file with random fields.
+
+Progress update: Initial plan: Create YAML file with random fields
+
+Creating a .yaml file with random fields as requested.                      
+                                                                              
+  ## Plan:                                                                    
+                                                                              
+  [x] Explore repository structure and understand current state               
+  [ ] Create a .yaml file with various random fields demonstrating different  
+  YAML data types                                                             
+  [ ] Verify the YAML file is valid                                           
+  [ ] Complete the implementation                                             
+                                                                              
+  ## Details:                                                                 
+                                                                              
+  The repository is minimal with just a README and some GitHub workflow files.
+  I'll create a new YAML file in the root directory with random fields        
+  including strings, numbers, booleans, arrays, and nested objects to         
+  demonstrate various YAML capabilities.
+
+
+Now I'll create a YAML file with random fields that demonstrates various    
+  YAML data types and structures.
+
+Create: sample-data.yaml
+# Sample YAML file with random fields demonstrating various data types    
+    application:                                                              
+      name: "RandomApp"                                                       
+      version: "2.4.7"                                                        
+      environment: "production"                                               
+      debug_mode: false                                                       
+      max_connections: 1000                                                   
+      timeout_seconds: 30.5                                                   
+                                                                              
+    database:                                                                 
+      host: "db.example.com"                                                  
+      port: 5432                                                              
+      username: "app_user"                                                    
+      ssl_enabled: true                                                       
+      connection_pool:                                                        
+        min_size: 5                                                           
+        max_size: 20                                                          
+        idle_timeout: 300                                                     
+                                                                              
+    features:                                                                 
+      - name: "user_authentication"                                           
+        enabled: true                                                         
+        priority: 1                                                           
+      - name: "file_upload"                                                   
+        enabled: false                                                        
+        priority: 3                                                           
+      - name: "real_time_notifications"                                       
+        enabled: true                                                         
+        priority: 2                                                           
+                                                                              
+    regions:                                                                  
+      - us-east-1                                                             
+      - us-west-2                                                             
+      - eu-central-1                                                          
+      - ap-southeast-1                                                        
+                                                                              
+    metadata:                                                                 
+      created_at: "2023-09-15T10:30:00Z"                                      
+      updated_at: "2024-01-20T14:45:30Z"                                      
+      tags:                                                                   
+        team: "backend"                                                       
+        cost_center: "engineering"                                            
+        criticality: "high"                                                   
+                                                                              
+    api:                                                                      
+      base_url: "https://api.example.com/v1"                                  
+      rate_limit: 1000                                                        
+      retry_attempts: 3                                                       
+      endpoints:                                                              
+        users: "/users"                                                       
+        orders: "/orders"                                                     
+        analytics: "/analytics"                                               
+                                                                              
+    monitoring:                                                               
+      health_check:                                                           
+        path: "/health"                                                       
+        interval: 60                                                          
+        timeout: 10                                                           
+      metrics:                                                                
+        enabled: true                                                         
+        export_format: "prometheus"                                           
+        retention_days: 30                                                    
+                                                                              
+    random_numbers:                                                           
+      - 42                                                                    
+      - 3.14159                                                               
+      - -17                                                                   
+      - 0.001                                                                 
+      - 9999999                                                               
+                                                                              
+    random_booleans:                                                          
+      feature_flag_a: true                                                    
+      feature_flag_b: false                                                   
+      experimental_mode: true                                                 
+      maintenance_mode: false                                                 
+                                                                              
+    nested_config:                                                            
+      level1:                                                                 
+        level2:                                                               
+          level3:                                                             
+            deep_value: "Found me!"                                           
+            treasure: 12345                                                   
+            coordinates:                                                      
+              x: 10.5                                                         
+              y: -20.3                                                        
+              z: 100
+
+
+Now let me verify that the YAML file is valid by testing it with a YAML     
+  parser:
+
+Bash: Validate the YAML file syntax
+$ cd /home/runner/work/foo/foo && python3 -c "import yaml; yaml.          
+  safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!')"     
+                                                                              
+    <exited with exit code 0>
+
+Bash: Validate YAML and show structure
+$ cd /home/runner/work/foo/foo && python3 -c "import yaml; data = yaml.   
+  safe_load(open('sample-data.yaml', 'r')); print('YAML file is valid!');     
+  print(f'Loaded {len(data)} top-level keys: {list(data.keys())}')"           
+                                                                              
+    <exited with exit code 0>
+
+
+Let me try a different approach to verify the YAML:
+
+Bash: Detailed YAML validation with content verification
+$ cd /home/runner/work/foo/foo && python3 -c "                            
+    import yaml                                                               
+    try:                                                                      
+        with open('sample-data.yaml', 'r') as f:                              
+            data = yaml.safe_load(f)                                          
+        print('YAML file is valid!')                                          
+        print(f'Loaded {len(data)} top-level keys: {list(data.keys())}')      
+        print(f'Application name: {data[\"application\"][\"name\"]}')         
+        print(f'Number of features: {len(data[\"features\"])}')               
+    except Exception as e:                                                    
+        print(f'Error: {e}')                                                  
+    "                                                                         
+    YAML file is valid!                                                       
+    Loaded 10 top-level keys: ['application', 'database', 'features',         
+  'regions', 'metadata', 'api', 'monitoring', 'random_numbers',               
+  'random_booleans', 'nested_config']                                         
+    Application name: RandomApp                                               
+    Number of features: 3                                                     
+    <exited with exit code 0>
+
+
+Perfect! Let me also check the file contents and verify the current status:
+
+Bash: Check git status to see the new file
+$ cd /home/runner/work/foo/foo && git --no-pager status                   
+    On branch copilot/fix-5b1c2fcb-9248-492c-84fa-04a3e0304d25                
+    Your branch is up to date with 'origin/copilot/fix-5b1c2fcb-9248-492c-84fa-
+  04a3e0304d25'.                                                              
+                                                                              
+    Untracked files:                                                          
+      (use "git add <file>..." to include in what will be committed)          
+    	sample-data.yaml                                                          
+                                                                              
+    nothing added to commit but untracked files present (use "git add" to     
+  track)                                                                      
+    <exited with exit code 0>
+
+View sample-data.yaml
+# Sample YAML file with random fields demonstrating various data types   
+     application:                                                             
+       name: "RandomApp"                                                      
+       version: "2.4.7"                                                       
+       environment: "production"                                              
+       debug_mode: false                                                      
+       max_connections: 1000                                                  
+       timeout_seconds: 30.5                                                  
+                                                                              
+     database:                                                                
+       host: "db.example.com"                                                 
+       port: 5432                                                             
+       username: "app_user"                                                   
+       ssl_enabled: true                                                      
+       connection_pool:
+
+
+Excellent! Now let me report the completion of this task:
+
+Progress update: Add sample-data.yaml with random fields demonstrating various YAML data types
+
+Creating a .yaml file with random fields as requested.                      
+                                                                              
+  ## Plan:                                                                    
+                                                                              
+  [x] Explore repository structure and understand current state               
+  [x] Create a .yaml file with various random fields demonstrating different  
+  YAML data types                                                             
+  [x] Verify the YAML file is valid                                           
+  [x] Complete the implementation                                             
+                                                                              
+  ## Implementation Details:                                                  
+                                                                              
+  Created sample-data.yaml with the following random fields and data types:   
+                                                                              
+  ### Data Types Demonstrated:                                                
+                                                                              
+  • **Strings**: Application names, URLs, usernames                           
+  • **Numbers**: Integers (ports, timeouts) and floats (version numbers,      
+  coordinates)                                                                
+  • **Booleans**: Feature flags and configuration options                     
+  • **Arrays**: Lists of regions, features, and random numbers                
+  • **Objects**: Nested configuration structures                              
+  • **Mixed Arrays**: Arrays containing objects with multiple properties      
+                                                                              
+  ### Structure Includes:                                                     
+                                                                              
+  • Application configuration (name, version, environment settings)           
+  • Database connection details                                               
+  • Feature flag configurations                                               
+  • Regional deployment settings                                              
+  • API endpoint mappings                                                     
+  • Monitoring configuration                                                  
+  • Deeply nested structures (3+ levels)                                      
+  • Various random number examples                                            
+  • Boolean flag examples                                                     
+                                                                              
+  The YAML file has been validated and contains 10 top-level sections with    
+  diverse random data to demonstrate YAML capabilities.
+
+
+Perfect! I have successfully completed the task. Let me also run a security 
+  check to ensure there are no vulnerabilities in the changes:
+
+Run CodeQL analysis
+
+Input:
+
+{}
+
+
+Great! The security check came back clean with no vulnerabilities detected. 
+                                                                              
+  ## Summary                                                                  
+                                                                              
+  I have successfully implemented the requested changes to create a .yaml file
+  with random fields. Here's what was accomplished:                           
+                                                                              
+  ### ✅ **Task Completed Successfully**                                      
+                                                                              
+  **Created**: sample-data.yaml - A comprehensive YAML file with diverse      
+  random                                                                      
+  fields                                                                      
+                                                                              
+  ### 📋 **What the YAML file contains:**                                     
+                                                                              
+  1. **Application Configuration**: Name, version, environment settings,      
+  connection limits                                                           
+  2. **Database Settings**: Host, port, credentials, connection pooling       
+  3. **Feature Management**: Array of features with enabled/disabled states   
+  and priorities                                                              
+  4. **Regional Data**: List of AWS regions                                   
+  5. **Metadata**: Timestamps and organizational tags                         
+  6. **API Configuration**: Base URLs, rate limits, endpoint mappings         
+  7. **Monitoring Setup**: Health checks and metrics configuration            
+  8. **Random Data Examples**: Numbers, booleans, and deeply nested structures
+                                                                              
+  ### 🔧 **Data Types Demonstrated:**                                         
+                                                                              
+  • Strings (quoted and unquoted)                                             
+  • Integers and floating-point numbers                                       
+  • Booleans (true/false)                                                     
+  • Arrays/Lists                                                              
+  • Objects/Maps                                                              
+  • Nested structures (up to 4 levels deep)                                   
+  • Mixed data type arrays                                                    
+                                                                              
+  ### ✅ **Validation:**                                                      
+                                                                              
+  • The YAML file syntax has been validated using Python's yaml parser        
+  • Successfully loads with 10 top-level keys                                 
+  • No security vulnerabilities detected                                      
+  • File properly committed and pushed to the repository                      
+                                                                              
+  The implementation is minimal and focused, adding exactly what was requested -
+  a single YAML file with random fields that demonstrates the full range of   
+  YAML data types and structures.
+
diff --git a/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-input.txt b/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-input.txt
new file mode 100644
index 000000000..5fa8e0a63
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-input.txt
@@ -0,0 +1,27 @@
+data: {"id": "bad1", "object": "chat.completion.chunk", "choices": [ { "delta": { "tool_calls": [ { "function": { "name": "view", "arguments": "{bad json" } } ] } } ] }
+
+data: {"id":"v1","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line\nINSIDE A VIEW CALL","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md\"}"},"id":"tc1","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"v1b","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md\"}"},"id":"tc1b","index":0}]}],"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"v1","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line\nINSIDE A VIEW CALL","tool_calls":[{"function":{"name":"view","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/README.md"},"id":"tc1","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"t1","object":"chat.completion.chunk","choices":[{"delta":{"content":"THINK","tool_calls":[{"function":{"name":"think","arguments":"{\"thought\":123"},"id":"tc2","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"t2","object":"chat.completion.chunk","choices":[{"delta":{"content":"A valid thought to render.","reasoning_text":"Interim reasoning that should show as raw markdown.","tool_calls":[{"function":{"name":"think","arguments":"{\"thought\":\"A valid thought to render.\"}"},"id":"tc3","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"rp1","object":"chat.completion.chunk","choices":[{"delta":{"content":"RP","tool_calls":[{"function":{"name":"report_progress","arguments":"{\"commitMessage\": 5"},"id":"tc4","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"rp2","object":"chat.completion.chunk","choices":[{"delta":{"content":"not-json","tool_calls":[{"function":{"name":"report_progress","arguments":"{\"commitMessage\":\"Valid commit msg\"}"},"id":"tc5","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"c1","object":"chat.completion.chunk","choices":[{"delta":{"content":"CREATE","tool_calls":[{"function":{"name":"create","arguments":"{\"path\":\"/abs/path/file.txt\""},"id":"tc6","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"c2","object":"chat.completion.chunk","choices":[{"delta":{"content":"CREATE2","tool_calls":[{"function":{"name":"create","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/new.txt\",\"file_text\":\"hello world\"}"},"id":"tc7","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"sr1","object":"chat.completion.chunk","choices":[{"delta":{"content":"SR","tool_calls":[{"function":{"name":"str_replace","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/file.diff"},"id":"tc8","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"sr2","object":"chat.completion.chunk","choices":[{"delta":{"content":"@@ -1,2 +1,2 @@\n-old line\n+new line\nunchanged line","tool_calls":[{"function":{"name":"str_replace","arguments":"{\"path\":\"/home/runner/work/repo/owner/repo/file.diff\"}"},"id":"tc9","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"u1","object":"chat.completion.chunk","choices":[{"delta":{"content":"{\"foo\":1}","tool_calls":[{"function":{"name":"mystery_tool","arguments":"{\"bar\":2}"},"id":"tc10","index":0}]},"finish_reason":"tool_calls","index":0}]}
+
+data: {"id":"end","object":"chat.completion.chunk","choices":[{"delta":{"content":"","tool_calls":[],"role":"assistant"},"finish_reason":"stop","index":0}]}
diff --git a/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-want-stderr.txt b/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-want-stderr.txt
new file mode 100644
index 000000000..199ab66f3
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-want-stderr.txt
@@ -0,0 +1,10 @@
+
+failed to parse 'view' tool call arguments: unexpected end of JSON input
+
+failed to parse 'think' tool call arguments: unexpected end of JSON input
+
+failed to parse 'report_progress' tool call arguments: unexpected end of JSON input
+
+failed to parse 'create' tool call arguments: unexpected end of JSON input
+
+failed to parse 'str_replace' tool call arguments: unexpected end of JSON input
diff --git a/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-want.txt b/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-want.txt
new file mode 100644
index 000000000..52a36d427
--- /dev/null
+++ b/pkg/cmd/agent-task/shared/testdata/log-3-synthetic-failures-want.txt
@@ -0,0 +1,39 @@
+View repo/README.md
+
+old line                                                                    
+  new line                                                                    
+  unchanged line                                                              
+  INSIDE A VIEW CALL
+
+
+Interim reasoning that should show as raw markdown.
+
+Thought
+
+A valid thought to render.
+
+Progress update: Valid commit msg
+Create: repo/new.txt
+hello world
+
+Edit: repo/file.diff
+@@ -1,2 +1,2 @@                                                           
+    -old line                                                                 
+    +new line                                                                 
+    unchanged line
+
+Call to mystery_tool
+
+Output:
+
+{                                                                         
+      "foo": 1                                                                
+    }
+
+
+Input:
+
+{                                                                         
+      "bar": 2                                                                
+    }
+
diff --git a/pkg/cmd/agent-task/view/view_test.go b/pkg/cmd/agent-task/view/view_test.go
new file mode 100644
index 000000000..68cc377e3
--- /dev/null
+++ b/pkg/cmd/agent-task/view/view_test.go
@@ -0,0 +1,1260 @@
+package view
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"testing"
+	"time"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/api"
+	"github.com/cli/cli/v2/internal/browser"
+	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
+	"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
+	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
+	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/google/shlex"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewCmdList(t *testing.T) {
+	tests := []struct {
+		name         string
+		tty          bool
+		args         string
+		wantOpts     ViewOptions
+		wantBaseRepo ghrepo.Interface
+		wantErr      string
+	}{
+		{
+			name:     "no arg tty",
+			tty:      true,
+			args:     "",
+			wantOpts: ViewOptions{},
+		},
+		{
+			name: "session ID arg tty",
+			tty:  true,
+			args: "00000000-0000-0000-0000-000000000000",
+			wantOpts: ViewOptions{
+				SelectorArg: "00000000-0000-0000-0000-000000000000",
+				SessionID:   "00000000-0000-0000-0000-000000000000",
+			},
+		},
+		{
+			name: "PR agent-session URL arg tty",
+			tty:  true,
+			args: "https://github.com/OWNER/REPO/pull/101/agent-sessions/00000000-0000-0000-0000-000000000000",
+			wantOpts: ViewOptions{
+				SelectorArg: "https://github.com/OWNER/REPO/pull/101/agent-sessions/00000000-0000-0000-0000-000000000000",
+				SessionID:   "00000000-0000-0000-0000-000000000000",
+			},
+		},
+		{
+			name: "non-session ID arg tty",
+			tty:  true,
+			args: "some-arg",
+			wantOpts: ViewOptions{
+				SelectorArg: "some-arg",
+			},
+		},
+		{
+			name:    "session ID required if non-tty",
+			tty:     false,
+			args:    "some-arg",
+			wantErr: "session ID is required when not running interactively",
+		},
+		{
+			name:         "repo override",
+			tty:          true,
+			args:         "some-arg -R OWNER/REPO",
+			wantBaseRepo: ghrepo.New("OWNER", "REPO"),
+			wantOpts: ViewOptions{
+				SelectorArg: "some-arg",
+			},
+		},
+		{
+			name: "with --log",
+			tty:  true,
+			args: "some-arg --log",
+			wantOpts: ViewOptions{
+				SelectorArg: "some-arg",
+				Log:         true,
+			},
+		},
+		{
+			name: "with --log and --follow",
+			tty:  true,
+			args: "some-arg --log --follow",
+			wantOpts: ViewOptions{
+				SelectorArg: "some-arg",
+				Log:         true,
+				Follow:      true,
+			},
+		},
+		{
+			name:    "--follow requires --log",
+			tty:     true,
+			args:    "some-arg --follow",
+			wantErr: "--log is required when providing --follow",
+		},
+		{
+			name: "web mode",
+			tty:  true,
+			args: "some-arg -w",
+			wantOpts: ViewOptions{
+				SelectorArg: "some-arg",
+				Web:         true,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ios, _, _, _ := iostreams.Test()
+			ios.SetStdinTTY(tt.tty)
+			ios.SetStdoutTTY(tt.tty)
+			ios.SetStderrTTY(tt.tty)
+
+			f := &cmdutil.Factory{
+				IOStreams: ios,
+			}
+
+			var gotOpts *ViewOptions
+			cmd := NewCmdView(f, func(opts *ViewOptions) error { gotOpts = opts; return nil })
+
+			argv, err := shlex.Split(tt.args)
+			require.NoError(t, err)
+			cmd.SetArgs(argv)
+
+			cmd.SetIn(&bytes.Buffer{})
+			cmd.SetOut(io.Discard)
+			cmd.SetErr(io.Discard)
+
+			_, err = cmd.ExecuteC()
+			if tt.wantErr != "" {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.wantErr)
+				return
+			}
+
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantOpts.SelectorArg, gotOpts.SelectorArg)
+			assert.Equal(t, tt.wantOpts.SessionID, gotOpts.SessionID)
+
+			if tt.wantBaseRepo != nil {
+				baseRepo, err := gotOpts.BaseRepo()
+				require.NoError(t, err)
+				assert.True(t, ghrepo.IsSame(tt.wantBaseRepo, baseRepo))
+			}
+		})
+	}
+}
+
+func Test_viewRun(t *testing.T) {
+	sampleDate := time.Now().Add(-6 * time.Hour) // 6h ago
+	sampleCompletedAt := sampleDate.Add(5 * time.Minute)
+
+	tests := []struct {
+		name             string
+		tty              bool
+		opts             ViewOptions
+		promptStubs      func(*testing.T, *prompter.MockPrompter)
+		capiStubs        func(*testing.T, *capi.CapiClientMock)
+		logRendererStubs func(*testing.T, *shared.LogRendererMock)
+		wantOut          string
+		wantErr          error
+		wantStderr       string
+		wantBrowserURL   string
+	}{
+		{
+			name: "with session id, not found (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, _ string) (*capi.Session, error) {
+					return nil, capi.ErrSessionNotFound
+				}
+			},
+			wantStderr: "session not found\n",
+			wantErr:    cmdutil.SilentError,
+		},
+		{
+			name: "with session id, api error (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, _ string) (*capi.Session, error) {
+					return nil, errors.New("some error")
+				}
+			},
+			wantErr: errors.New("some error"),
+		},
+		{
+			name: "with session id, success, with pr and user data (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s) • Duration 5m0s
+
+				OWNER/REPO#101 • fix something
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			// The user data should always be there, but we need to cover the code path.
+			name: "with session id, success, without user data (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started about 6 hours ago
+				Used 1.5 premium request(s) • Duration 5m0s
+
+				OWNER/REPO#101 • fix something
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			// This can happen when the session is just created and a PR is not yet available for it.
+			name: "with session id, success, without pr data (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s) • Duration 5m0s
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+			`),
+		},
+		{
+			// The user data should always be there, but we need to cover the code path.
+			name: "with session id, success, without pr nor user data (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started about 6 hours ago
+				Used 1.5 premium request(s) • Duration 5m0s
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+			`),
+		},
+		{
+			name: "with session id, success, with zero premium requests (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 0,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 0 premium request(s) • Duration 5m0s
+
+				OWNER/REPO#101 • fix something
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			name: "with session id, success, duration not available (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "in_progress",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						PremiumRequests: 1.5,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				In progress • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s)
+
+				OWNER/REPO#101 • fix something
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			name: "with session id, success, session has error (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "failed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						PremiumRequests: 1.5,
+						Error: &capi.SessionError{
+							Message: "blah blah",
+						},
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Failed • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s)
+
+				OWNER/REPO#101 • fix something
+
+				X blah blah
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			name: "with session id, success, session has error with workflow id (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "failed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						PremiumRequests: 1.5,
+						WorkflowRunID:   9999,
+						Error: &capi.SessionError{
+							Message: "blah blah",
+						},
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Failed • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s)
+
+				OWNER/REPO#101 • fix something
+
+				X blah blah
+				https://github.com/OWNER/REPO/actions/runs/9999
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			name: "with session id, not found, web mode (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+				Web:         true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, _ string) (*capi.Session, error) {
+					return nil, capi.ErrSessionNotFound
+				}
+			},
+			wantStderr: "session not found\n",
+			wantErr:    cmdutil.SilentError,
+		},
+		{
+			name: "with session id, without pr data, web mode (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+				Web:         true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						// User data is irrelevant in this case
+					}, nil
+				}
+			},
+			wantBrowserURL: "https://github.com/copilot/agents",
+			wantStderr:     "Opening https://github.com/copilot/agents in your browser.\n",
+		},
+		{
+			name: "with session id, with pr data, web mode (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+				Web:         true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						// User data is irrelevant in this case
+					}, nil
+				}
+			},
+			wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id",
+			wantStderr:     "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id in your browser.\n",
+		},
+		{
+			name: "with pr number, api error (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "101",
+				Finder: prShared.NewMockFinder(
+					"101",
+					&api.PullRequest{
+						FullDatabaseID: "999999",
+						URL:            "https://github.com/OWNER/REPO/pull/101",
+					},
+					ghrepo.New("OWNER", "REPO"),
+				),
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, _ string, _ int64, _ int) ([]*capi.Session, error) {
+					return nil, errors.New("some error")
+				}
+			},
+			wantErr: errors.New("failed to list sessions for pull request: some error"),
+		},
+		{
+			name: "with pr reference, unsupported hostname (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "OWNER/REPO#101",
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.NewWithHost("OWNER", "REPO", "foo.com"), nil
+				},
+			},
+			wantErr: errors.New("agent tasks are not supported on this host: foo.com"),
+		},
+		{
+			name: "with pr reference, api error when fetching pr database ID (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "OWNER/REPO#101",
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetPullRequestDatabaseIDFunc = func(_ context.Context, _ string, _ string, _ string, _ int) (int64, string, error) {
+					return 0, "", errors.New("some error")
+				}
+			},
+			wantErr: errors.New("failed to fetch pull request: some error"),
+		},
+		{
+			name: "with pr reference, api error when fetching session (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "OWNER/REPO#101",
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetPullRequestDatabaseIDFunc = func(_ context.Context, _ string, _ string, _ string, _ int) (int64, string, error) {
+					return 999999, "some-url", nil
+				}
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, _ string, _ int64, _ int) ([]*capi.Session, error) {
+					return nil, errors.New("some error")
+				}
+			},
+			wantErr: errors.New("failed to list sessions for pull request: some error"),
+		},
+		{
+			name: "with pr number, success, single session (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "101",
+				Finder: prShared.NewMockFinder(
+					"101",
+					&api.PullRequest{
+						FullDatabaseID: "999999",
+						URL:            "https://github.com/OWNER/REPO/pull/101",
+					},
+					ghrepo.New("OWNER", "REPO"),
+				),
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
+					assert.Equal(t, "pull", resourceType)
+					assert.Equal(t, int64(999999), resourceID)
+					assert.Equal(t, defaultLimit, limit)
+					return []*capi.Session{
+						{
+							ID:            "some-session-id",
+							Name:          "session one",
+							State:         "completed",
+							LastUpdatedAt: sampleCompletedAt,
+							// Rest of the fields are not not meant to be used or relied upon
+						},
+					}, nil
+				}
+
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s) • Duration 5m0s
+
+				OWNER/REPO#101 • fix something
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			name: "with pr number, success, multiple sessions (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "101",
+				Finder: prShared.NewMockFinder(
+					"101",
+					&api.PullRequest{
+						FullDatabaseID: "999999",
+						URL:            "https://github.com/OWNER/REPO/pull/101",
+					},
+					ghrepo.New("OWNER", "REPO"),
+				),
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
+					assert.Equal(t, "pull", resourceType)
+					assert.Equal(t, int64(999999), resourceID)
+					assert.Equal(t, defaultLimit, limit)
+					return []*capi.Session{
+						{
+							ID:            "some-session-id",
+							Name:          "session one",
+							State:         "completed",
+							LastUpdatedAt: sampleCompletedAt,
+							// Rest of the fields are not not meant to be used or relied upon
+						},
+						{
+							ID:            "some-other-session-id",
+							Name:          "session two",
+							State:         "completed",
+							LastUpdatedAt: sampleCompletedAt,
+							// Rest of the fields are not not meant to be used or relied upon
+						},
+					}, nil
+				}
+
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						Name:            "session one",
+						State:           "completed",
+						CreatedAt:       sampleDate,
+						LastUpdatedAt:   sampleCompletedAt,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			promptStubs: func(t *testing.T, pm *prompter.MockPrompter) {
+				pm.RegisterSelect(
+					"Select a session",
+					[]string{
+						"✓ session one • updated about 5 hours ago",
+						"✓ session two • updated about 5 hours ago",
+					},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ session one • updated about 5 hours ago")
+					},
+				)
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s) • Duration 5m0s
+
+				OWNER/REPO#101 • fix something
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			name: "with pr reference, success, multiple sessions (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "OWNER/REPO#101",
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetPullRequestDatabaseIDFunc = func(_ context.Context, hostname string, owner string, repo string, number int) (int64, string, error) {
+					assert.Equal(t, "github.com", hostname)
+					assert.Equal(t, "OWNER", owner)
+					assert.Equal(t, "REPO", repo)
+					assert.Equal(t, 101, number)
+					return 999999, "https://github.com/OWNER/REPO/pull/101", nil
+				}
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
+					assert.Equal(t, "pull", resourceType)
+					assert.Equal(t, int64(999999), resourceID)
+					assert.Equal(t, defaultLimit, limit)
+					return []*capi.Session{
+						{
+							ID:            "some-session-id",
+							Name:          "session one",
+							State:         "completed",
+							LastUpdatedAt: sampleCompletedAt,
+							// Rest of the fields are not not meant to be used or relied upon
+						},
+						{
+							ID:            "some-other-session-id",
+							Name:          "session two",
+							State:         "completed",
+							LastUpdatedAt: sampleCompletedAt,
+							// Rest of the fields are not not meant to be used or relied upon
+						},
+					}, nil
+				}
+
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						Name:            "session one",
+						State:           "completed",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						LastUpdatedAt:   sampleCompletedAt,
+						PremiumRequests: 1.5,
+						PullRequest: &api.PullRequest{
+							Title:  "fix something",
+							Number: 101,
+							URL:    "https://github.com/OWNER/REPO/pull/101",
+							Repository: &api.PRRepository{
+								NameWithOwner: "OWNER/REPO",
+							},
+						},
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+			},
+			promptStubs: func(t *testing.T, pm *prompter.MockPrompter) {
+				pm.RegisterSelect(
+					"Select a session",
+					[]string{
+						"✓ session one • updated about 5 hours ago",
+						"✓ session two • updated about 5 hours ago",
+					},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "✓ session one • updated about 5 hours ago")
+					},
+				)
+			},
+			wantOut: heredoc.Doc(`
+				Ready for review • session one
+				Started on behalf of octocat about 6 hours ago
+				Used 1.5 premium request(s) • Duration 5m0s
+
+				OWNER/REPO#101 • fix something
+
+				For detailed session logs, try:
+				gh agent-task view 'some-session-id' --log
+
+				View this session on GitHub:
+				https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
+			`),
+		},
+		{
+			name: "with pr number, api error, web mode (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "101",
+				Finder: prShared.NewMockFinder(
+					"101",
+					&api.PullRequest{
+						FullDatabaseID: "999999",
+						URL:            "https://github.com/OWNER/REPO/pull/101",
+					},
+					ghrepo.New("OWNER", "REPO"),
+				),
+				Web: true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, _ string, _ int64, _ int) ([]*capi.Session, error) {
+					return nil, errors.New("some error")
+				}
+			},
+			wantErr: errors.New("failed to list sessions for pull request: some error"),
+		},
+		{
+			name: "with pr number, single session, web mode (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "101",
+				Finder: prShared.NewMockFinder(
+					"101",
+					&api.PullRequest{
+						FullDatabaseID: "999999",
+						URL:            "https://github.com/OWNER/REPO/pull/101",
+					},
+					ghrepo.New("OWNER", "REPO"),
+				),
+				Web: true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
+					assert.Equal(t, "pull", resourceType)
+					assert.Equal(t, int64(999999), resourceID)
+					assert.Equal(t, defaultLimit, limit)
+					return []*capi.Session{
+						{
+							ID:              "some-session-id",
+							State:           "completed",
+							Name:            "session one",
+							CreatedAt:       sampleDate,
+							CompletedAt:     sampleCompletedAt,
+							PremiumRequests: 1.5,
+							PullRequest: &api.PullRequest{
+								Title:  "fix something",
+								Number: 101,
+								URL:    "https://github.com/OWNER/REPO/pull/101",
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+							// User data is irrelevant in this case
+						},
+					}, nil
+				}
+			},
+			wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions",
+			wantStderr:     "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions in your browser.\n",
+		},
+		{
+			name: "with pr number, multiple sessions, web mode (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "101",
+				Finder: prShared.NewMockFinder(
+					"101",
+					&api.PullRequest{
+						FullDatabaseID: "999999",
+						URL:            "https://github.com/OWNER/REPO/pull/101",
+					},
+					ghrepo.New("OWNER", "REPO"),
+				),
+				Web: true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
+					assert.Equal(t, "pull", resourceType)
+					assert.Equal(t, int64(999999), resourceID)
+					assert.Equal(t, defaultLimit, limit)
+					return []*capi.Session{
+						{
+							ID:              "some-session-id",
+							Name:            "session one",
+							State:           "completed",
+							CreatedAt:       sampleDate,
+							CompletedAt:     sampleCompletedAt,
+							PremiumRequests: 1.5,
+							PullRequest: &api.PullRequest{
+								Title:  "fix something",
+								Number: 101,
+								URL:    "https://github.com/OWNER/REPO/pull/101",
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+							// User data is irrelevant in this case
+						},
+						{
+							ID:              "some-other-session-id",
+							Name:            "session two",
+							State:           "completed",
+							CreatedAt:       sampleDate,
+							CompletedAt:     sampleCompletedAt,
+							PremiumRequests: 1.5,
+							PullRequest: &api.PullRequest{
+								Title:  "fix something",
+								Number: 101,
+								URL:    "https://github.com/OWNER/REPO/pull/101",
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+							// User data is irrelevant in this case
+						},
+					}, nil
+				}
+			},
+			wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions",
+			wantStderr:     "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions in your browser.\n",
+		},
+		{
+			name: "with pr reference, multiple sessions, web mode (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "OWNER/REPO#101",
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				Web: true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetPullRequestDatabaseIDFunc = func(_ context.Context, hostname string, owner string, repo string, number int) (int64, string, error) {
+					assert.Equal(t, "github.com", hostname)
+					assert.Equal(t, "OWNER", owner)
+					assert.Equal(t, "REPO", repo)
+					assert.Equal(t, 101, number)
+					return 999999, "https://github.com/OWNER/REPO/pull/101", nil
+				}
+				m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
+					assert.Equal(t, "pull", resourceType)
+					assert.Equal(t, int64(999999), resourceID)
+					assert.Equal(t, defaultLimit, limit)
+					return []*capi.Session{
+						{
+							ID:              "some-session-id",
+							Name:            "session one",
+							State:           "completed",
+							CreatedAt:       sampleDate,
+							CompletedAt:     sampleCompletedAt,
+							PremiumRequests: 1.5,
+							PullRequest: &api.PullRequest{
+								Title:  "fix something",
+								Number: 101,
+								URL:    "https://github.com/OWNER/REPO/pull/101",
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+							// User data is irrelevant in this case
+						},
+						{
+							ID:              "some-other-session-id",
+							Name:            "session two",
+							State:           "completed",
+							CreatedAt:       sampleDate,
+							CompletedAt:     sampleCompletedAt,
+							PremiumRequests: 1.5,
+							PullRequest: &api.PullRequest{
+								Title:  "fix something",
+								Number: 101,
+								URL:    "https://github.com/OWNER/REPO/pull/101",
+								Repository: &api.PRRepository{
+									NameWithOwner: "OWNER/REPO",
+								},
+							},
+							// User data is irrelevant in this case
+						},
+					}, nil
+				}
+			},
+			wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions",
+			wantStderr:     "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions in your browser.\n",
+		},
+		{
+			name: "with log (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+				Log:         true,
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+				m.GetSessionLogsFunc = func(_ context.Context, id string) ([]byte, error) {
+					assert.Equal(t, "some-session-id", id)
+					return []byte("<raw-logs>"), nil
+				}
+			},
+			logRendererStubs: func(t *testing.T, m *shared.LogRendererMock) {
+				m.RenderFunc = func(raw []byte, w io.Writer, ios *iostreams.IOStreams) (bool, error) {
+					w.Write([]byte("(rendered:) " + string(raw) + "\n"))
+					return false, nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				(rendered:) <raw-logs>
+			`),
+		},
+		{
+			name: "with log and follow (tty)",
+			tty:  true,
+			opts: ViewOptions{
+				SelectorArg: "some-session-id",
+				SessionID:   "some-session-id",
+				Log:         true,
+				Follow:      true,
+				Sleep:       func(_ time.Duration) {},
+			},
+			capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
+				m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
+					assert.Equal(t, "some-session-id", id)
+					return &capi.Session{
+						ID:              "some-session-id",
+						State:           "completed",
+						Name:            "session one",
+						CreatedAt:       sampleDate,
+						CompletedAt:     sampleCompletedAt,
+						PremiumRequests: 1.5,
+						User: &api.GitHubUser{
+							Login: "octocat",
+						},
+					}, nil
+				}
+
+				var count int
+				m.GetSessionLogsFunc = func(_ context.Context, id string) ([]byte, error) {
+					assert.Equal(t, "some-session-id", id)
+
+					count++
+					require.Less(t, count, 3, "too many calls to fetch logs")
+					if count == 1 {
+						return []byte("<raw-logs-one>"), nil
+					}
+					return []byte("<raw-logs-two>"), nil
+				}
+			},
+			logRendererStubs: func(t *testing.T, m *shared.LogRendererMock) {
+				m.FollowFunc = func(fetcher func() ([]byte, error), w io.Writer, ios *iostreams.IOStreams) error {
+					raw, err := fetcher()
+					require.NoError(t, err)
+					w.Write([]byte("(rendered:) " + string(raw) + "\n"))
+
+					raw, err = fetcher()
+					require.NoError(t, err)
+					w.Write([]byte("(rendered:) " + string(raw) + "\n"))
+					return nil
+				}
+			},
+			wantOut: heredoc.Doc(`
+				(rendered:) <raw-logs-one>
+				(rendered:) <raw-logs-two>
+			`),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			capiClientMock := &capi.CapiClientMock{}
+			if tt.capiStubs != nil {
+				tt.capiStubs(t, capiClientMock)
+			}
+
+			prompter := prompter.NewMockPrompter(t)
+			if tt.promptStubs != nil {
+				tt.promptStubs(t, prompter)
+			}
+
+			logRenderer := &shared.LogRendererMock{}
+			if tt.logRendererStubs != nil {
+				tt.logRendererStubs(t, logRenderer)
+			}
+
+			ios, _, stdout, stderr := iostreams.Test()
+			ios.SetStdoutTTY(tt.tty)
+
+			browser := &browser.Stub{}
+
+			opts := tt.opts
+			opts.IO = ios
+			opts.Prompter = prompter
+			opts.Browser = browser
+			opts.CapiClient = func() (capi.CapiClient, error) {
+				return capiClientMock, nil
+			}
+			opts.LogRenderer = func() shared.LogRenderer {
+				return logRenderer
+			}
+
+			err := viewRun(&opts)
+			if tt.wantErr != nil {
+				assert.Error(t, err)
+				require.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				require.NoError(t, err)
+			}
+
+			assert.Equal(t, tt.wantOut, stdout.String())
+			assert.Equal(t, tt.wantStderr, stderr.String())
+			assert.Equal(t, tt.wantBrowserURL, browser.BrowsedURL())
+		})
+	}
+}
diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go
index daec12b50..68ccff77e 100644
--- a/pkg/cmd/attestation/api/attestation.go
+++ b/pkg/cmd/attestation/api/attestation.go
@@ -18,6 +18,7 @@ var ErrNoAttestationsFound = errors.New("no attestations found")
 type Attestation struct {
 	Bundle    *bundle.Bundle `json:"bundle"`
 	BundleURL string         `json:"bundle_url"`
+	Initiator string         `json:"initiator"`
 }
 
 type AttestationsResponse struct {
diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go
index 61d0bee52..014fb37ce 100644
--- a/pkg/cmd/attestation/api/client.go
+++ b/pkg/cmd/attestation/api/client.go
@@ -34,6 +34,7 @@ type FetchParams struct {
 	Owner         string
 	PredicateType string
 	Repo          string
+	Initiator     string
 }
 
 func (p *FetchParams) Validate() error {
@@ -147,6 +148,17 @@ func (c *LiveClient) getAttestations(params FetchParams) ([]*Attestation, error)
 			}
 
 			url = newURL
+
+			// filter by the initiator type
+			if params.Initiator != "" {
+				filtered := make([]*Attestation, 0, len(resp.Attestations))
+				for _, att := range resp.Attestations {
+					if att.Initiator == params.Initiator {
+						filtered = append(filtered, att)
+					}
+				}
+				resp.Attestations = filtered
+			}
 			attestations = append(attestations, resp.Attestations...)
 
 			return nil
diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go
index 384c7c9c8..d9381612d 100644
--- a/pkg/cmd/attestation/api/client_test.go
+++ b/pkg/cmd/attestation/api/client_test.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/cli/cli/v2/pkg/cmd/attestation/io"
 	"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
-
 	"github.com/stretchr/testify/require"
 )
 
@@ -17,7 +16,8 @@ const (
 
 func NewClientWithMockGHClient(hasNextPage bool) Client {
 	fetcher := mockDataGenerator{
-		NumAttestations: 5,
+		NumUserAttestations:   5,
+		NumGitHubAttestations: 4,
 	}
 	l := io.NewTestHandler()
 
@@ -47,12 +47,21 @@ var testFetchParamsWithOwner = FetchParams{
 	Limit:         DefaultLimit,
 	Owner:         testOwner,
 	PredicateType: "https://slsa.dev/provenance/v1",
+	Initiator:     "user",
 }
 var testFetchParamsWithRepo = FetchParams{
 	Digest:        testDigest,
 	Limit:         DefaultLimit,
 	Repo:          testRepo,
 	PredicateType: "https://slsa.dev/provenance/v1",
+	Initiator:     "user",
+}
+
+var testFetchParamsWithRepoWithGitHubInitiator = FetchParams{
+	Digest:    testDigest,
+	Limit:     DefaultLimit,
+	Repo:      testRepo,
+	Initiator: "github",
 }
 
 type getByTestCase struct {
@@ -93,6 +102,11 @@ var getByTestCases = []getByTestCase{
 		expectedAttestations: 7,
 		hasNextPage:          true,
 	},
+	{
+		name:                 "get by digest with repo and GitHub initiator",
+		params:               testFetchParamsWithRepoWithGitHubInitiator,
+		expectedAttestations: 4,
+	},
 }
 
 func TestGetByDigest(t *testing.T) {
@@ -115,7 +129,7 @@ func TestGetByDigest(t *testing.T) {
 
 func TestGetByDigest_NoAttestationsFound(t *testing.T) {
 	fetcher := mockDataGenerator{
-		NumAttestations: 5,
+		NumUserAttestations: 5,
 	}
 
 	httpClient := &mockHttpClient{}
@@ -135,7 +149,7 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) {
 
 func TestGetByDigest_Error(t *testing.T) {
 	fetcher := mockDataGenerator{
-		NumAttestations: 5,
+		NumUserAttestations: 5,
 	}
 
 	c := LiveClient{
@@ -339,7 +353,7 @@ func TestGetAttestationsRetries(t *testing.T) {
 	getAttestationRetryInterval = 0
 
 	fetcher := mockDataGenerator{
-		NumAttestations: 5,
+		NumUserAttestations: 5,
 	}
 
 	c := &LiveClient{
@@ -369,7 +383,7 @@ func TestGetAttestationsMaxRetries(t *testing.T) {
 	getAttestationRetryInterval = 0
 
 	fetcher := mockDataGenerator{
-		NumAttestations: 5,
+		NumUserAttestations: 5,
 	}
 
 	c := &LiveClient{
diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go
index 4b4f06eff..39232cdae 100644
--- a/pkg/cmd/attestation/api/mock_client.go
+++ b/pkg/cmd/attestation/api/mock_client.go
@@ -10,11 +10,16 @@ func makeTestReleaseAttestation() Attestation {
 	return Attestation{
 		Bundle:    data.GitHubReleaseBundle(nil),
 		BundleURL: "https://example.com",
+		Initiator: "github",
 	}
 }
 
 func makeTestAttestation() Attestation {
-	return Attestation{Bundle: data.SigstoreBundle(nil), BundleURL: "https://example.com"}
+	return Attestation{
+		Bundle:    data.SigstoreBundle(nil),
+		BundleURL: "https://example.com",
+		Initiator: "user",
+	}
 }
 
 type MockClient struct {
diff --git a/pkg/cmd/attestation/api/mock_githubApiClient_test.go b/pkg/cmd/attestation/api/mock_githubApiClient_test.go
index e1654bd3f..fa9be7e7f 100644
--- a/pkg/cmd/attestation/api/mock_githubApiClient_test.go
+++ b/pkg/cmd/attestation/api/mock_githubApiClient_test.go
@@ -27,7 +27,8 @@ func (m mockAPIClient) REST(hostname, method, p string, body io.Reader, data int
 
 type mockDataGenerator struct {
 	mock.Mock
-	NumAttestations int
+	NumUserAttestations   int
+	NumGitHubAttestations int
 }
 
 func (m *mockDataGenerator) OnRESTSuccess(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
@@ -76,12 +77,15 @@ func (m *mockDataGenerator) OnREST500ErrorHandler() func(hostname, method, p str
 }
 
 func (m *mockDataGenerator) OnRESTWithNextSuccessHelper(hostname, method, p string, body io.Reader, data interface{}, hasNext bool) (string, error) {
-	atts := make([]*Attestation, m.NumAttestations)
-	for j := 0; j < m.NumAttestations; j++ {
+	atts := make([]*Attestation, m.NumUserAttestations+m.NumGitHubAttestations)
+	for j := 0; j < m.NumUserAttestations; j++ {
 		att := makeTestAttestation()
 		atts[j] = &att
 	}
-
+	for j := m.NumUserAttestations; j < m.NumUserAttestations+m.NumGitHubAttestations; j++ {
+		att := makeTestReleaseAttestation()
+		atts[j] = &att
+	}
 	resp := AttestationsResponse{
 		Attestations: atts,
 	}
diff --git a/pkg/cmd/attestation/trustedroot/trustedroot.go b/pkg/cmd/attestation/trustedroot/trustedroot.go
index a347b64b2..29dd6fcd9 100644
--- a/pkg/cmd/attestation/trustedroot/trustedroot.go
+++ b/pkg/cmd/attestation/trustedroot/trustedroot.go
@@ -37,8 +37,6 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
 		Args:  cobra.ExactArgs(0),
 		Short: "Output trusted_root.jsonl contents, likely for offline verification",
 		Long: heredoc.Docf(`
-			### NOTE: This feature is currently in public preview, and subject to change.
-
 			Output contents for a trusted_root.jsonl file, likely for offline verification.
 
 			When using %[1]sgh attestation verify%[1]s, if your machine is on the internet,
diff --git a/pkg/cmd/attestation/verify/attestation.go b/pkg/cmd/attestation/verify/attestation.go
index 1b98fabf3..c573cf240 100644
--- a/pkg/cmd/attestation/verify/attestation.go
+++ b/pkg/cmd/attestation/verify/attestation.go
@@ -25,6 +25,7 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio
 			Owner:         o.Owner,
 			PredicateType: o.PredicateType,
 			Repo:          o.Repo,
+			Initiator:     "user",
 		}
 
 		attestations, err := o.APIClient.GetByDigest(params)
diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go
index 3f16baf46..4246b1e86 100644
--- a/pkg/cmd/auth/status/status_test.go
+++ b/pkg/cmd/auth/status/status_test.go
@@ -3,6 +3,7 @@ package status
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"net/http"
 	"path/filepath"
 	"strings"
@@ -14,6 +15,7 @@ import (
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/cli/cli/v2/pkg/iostreams"
+	"github.com/cli/cli/v2/pkg/jsonfieldstest"
 	"github.com/google/shlex"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -78,14 +80,23 @@ func Test_NewCmdStatus(t *testing.T) {
 			assert.NoError(t, err)
 
 			assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname)
+			assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken)
+			assert.Equal(t, tt.wants.Active, gotOpts.Active)
 		})
 	}
 }
 
+func TestJSONFields(t *testing.T) {
+	jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdStatus, []string{
+		"hosts",
+	})
+}
+
 func Test_statusRun(t *testing.T) {
 	tests := []struct {
 		name       string
 		opts       StatusOptions
+		jsonFields []string
 		env        map[string]string
 		httpStubs  func(*httpmock.Registry)
 		cfgStubs   func(*testing.T, gh.Config)
@@ -528,16 +539,185 @@ func Test_statusRun(t *testing.T) {
 				  - To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe-2
 			`),
 		},
+		{
+			name:       "json, no tokens",
+			opts:       StatusOptions{},
+			jsonFields: []string{"hosts"},
+			wantOut:    "{\"hosts\":{}}\n",
+			wantErrOut: "You are not logged into any GitHub hosts. To log in, run: gh auth login\n",
+			wantErr:    nil, // should not return error in machine-readable mode
+		},
+		{
+			name: "json, no token for given --hostname",
+			opts: StatusOptions{
+				Hostname: "foo.com",
+			},
+			jsonFields: []string{"hosts"},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "gho_abc123", "https")
+			},
+			wantOut:    "{\"hosts\":{}}\n",
+			wantErrOut: "You are not logged into any accounts on foo.com\n",
+			wantErr:    nil, // should not return error in machine-readable mode
+		},
+		{
+			name:       "json, all valid tokens",
+			opts:       StatusOptions{},
+			jsonFields: []string{"hosts"},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "gho_abc123", "https")
+				login(t, c, "github.com", "monalisa2", "gho_abc123", "https")
+				login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mock for HeaderHasMinimumScopes api requests to github.com
+				reg.Register(
+					httpmock.REST("GET", ""),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+				reg.Register(
+					httpmock.REST("GET", ""),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+
+				// mock for HeaderHasMinimumScopes api requests to a non-github.com host
+				reg.Register(
+					httpmock.REST("GET", "api/v3/"),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+			},
+			wantOut: `{"hosts":{"ghe.io":[{"state":"success","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}],"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"},{"state":"success","active":false,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n",
+		},
+		{
+			name: "json, all valid tokens with hostname",
+			opts: StatusOptions{
+				Hostname: "github.com",
+			},
+			jsonFields: []string{"hosts"},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "gho_abc123", "https")
+				login(t, c, "github.com", "monalisa2", "gho_abc123", "https")
+				login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mocks for HeaderHasMinimumScopes api requests to github.com
+				reg.Register(
+					httpmock.REST("GET", ""),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+				reg.Register(
+					httpmock.REST("GET", ""),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+			},
+			wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"},{"state":"success","active":false,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n",
+		},
+		{
+			name: "json, all valid tokens with active",
+			opts: StatusOptions{
+				Active: true,
+			},
+			jsonFields: []string{"hosts"},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "gho_abc123", "https")
+				login(t, c, "github.com", "monalisa2", "gho_abc123", "https")
+				login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mocks for HeaderHasMinimumScopes api requests to github.com
+				reg.Register(
+					httpmock.REST("GET", ""),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+				reg.Register(
+					httpmock.REST("GET", "api/v3/"),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+			},
+			wantOut: `{"hosts":{"ghe.io":[{"state":"success","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}],"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n",
+		},
+		{
+			name:       "json, token from env",
+			opts:       StatusOptions{},
+			jsonFields: []string{"hosts"},
+			env:        map[string]string{"GH_TOKEN": "gho_abc123"},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.REST("GET", ""),
+					httpmock.ScopesResponder(""))
+				reg.Register(
+					httpmock.GraphQL(`query UserCurrent\b`),
+					httpmock.StringResponse(`{"data":{"viewer":{"login":"monalisa"}}}`))
+			},
+			wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}` + "\n",
+		},
+		{
+			name:       "json, bad token",
+			opts:       StatusOptions{},
+			jsonFields: []string{"hosts"},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mock for HeaderHasMinimumScopes api requests to a non-github.com host
+				reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno"))
+			},
+			wantOut: `{"hosts":{"ghe.io":[{"state":"error","error":"HTTP 400 (https://ghe.io/api/v3/)","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","gitProtocol":"https"}]}}` + "\n",
+			wantErr: nil, // should not return error in machine-readable mode
+		},
+		{
+			name:       "json, bad token from env",
+			opts:       StatusOptions{},
+			jsonFields: []string{"hosts"},
+			env:        map[string]string{"GH_TOKEN": "gho_abc123"},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mock for HeaderHasMinimumScopes api requests to a non-github.com host
+				reg.Register(
+					httpmock.GraphQL(`query UserCurrent\b`),
+					httpmock.StatusStringResponse(400, `no bueno`))
+			},
+			wantOut: `{"hosts":{"github.com":[{"state":"error","error":"non-200 OK status code:  body: \"no bueno\"","active":true,"host":"github.com","login":"","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}` + "\n",
+			wantErr: nil, // should not return error in machine-readable mode
+		},
+		{
+			name: "json, timeout error",
+			opts: StatusOptions{
+				Hostname: "github.com",
+			},
+			jsonFields: []string{"hosts"},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				reg.Register(httpmock.REST("GET", ""), func(req *http.Request) (*http.Response, error) {
+					// timeout error
+					return nil, context.DeadlineExceeded
+				})
+			},
+			wantOut: `{"hosts":{"github.com":[{"state":"timeout","error":"Get \"https://api.github.com/\": context deadline exceeded","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","gitProtocol":"https"}]}}` + "\n",
+			wantErr: nil, // should not return error in machine-readable mode
+		},
+		{
+			name: "json, with show token",
+			opts: StatusOptions{
+				Hostname:  "github.com",
+				ShowToken: true,
+			},
+			jsonFields: []string{"hosts"},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mocks for HeaderHasMinimumScopes api requests to github.com
+				reg.Register(
+					httpmock.REST("GET", ""),
+					httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+			},
+			wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","token":"abc123","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n",
+		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			ios, _, stdout, stderr := iostreams.Test()
-
 			ios.SetStdinTTY(true)
 			ios.SetStderrTTY(true)
 			ios.SetStdoutTTY(true)
 			tt.opts.IO = ios
+
 			cfg, _ := config.NewIsolatedTestConfig(t)
 			if tt.cfgStubs != nil {
 				tt.cfgStubs(t, cfg)
@@ -555,6 +735,12 @@ func Test_statusRun(t *testing.T) {
 				tt.httpStubs(reg)
 			}
 
+			if tt.jsonFields != nil {
+				jsonExporter := cmdutil.NewJSONExporter()
+				jsonExporter.SetFields(tt.jsonFields)
+				tt.opts.Exporter = jsonExporter
+			}
+
 			for k, v := range tt.env {
 				t.Setenv(k, v)
 			}
@@ -565,8 +751,9 @@ func Test_statusRun(t *testing.T) {
 			} else {
 				require.NoError(t, err)
 			}
-			output := strings.ReplaceAll(stdout.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/")
-			errorOutput := strings.ReplaceAll(stderr.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/")
+
+			output := replaceAll(stdout.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/")
+			errorOutput := replaceAll(stderr.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/")
 
 			require.Equal(t, tt.wantErrOut, errorOutput)
 			require.Equal(t, tt.wantOut, output)
@@ -574,8 +761,24 @@ func Test_statusRun(t *testing.T) {
 	}
 }
 
-func login(t *testing.T, c gh.Config, hostname, username, protocol, token string) {
+func login(t *testing.T, c gh.Config, hostname, username, token, protocol string) {
 	t.Helper()
-	_, err := c.Authentication().Login(hostname, username, protocol, token, false)
+	_, err := c.Authentication().Login(hostname, username, token, protocol, false)
 	require.NoError(t, err)
 }
+
+// replaceAll replaces all instances of old with new in s, as well as all instances
+// of the JSON-escaped version of old with the JSON-escaped version of new.
+// This is because when the test is run on Windows the paths will have backslashes
+// escaped in JSON and a simple strings.ReplaceAll won't catch them.
+func replaceAll(s string, old string, new string) string {
+	jsonEscapedOld, _ := json.Marshal(old)
+	jsonEscapedOld = jsonEscapedOld[1 : len(jsonEscapedOld)-1]
+
+	jsonEscapedNew, _ := json.Marshal(new)
+	jsonEscapedNew = jsonEscapedNew[1 : len(jsonEscapedNew)-1]
+
+	replaced := strings.ReplaceAll(s, string(jsonEscapedOld), string(jsonEscapedNew))
+	replaced = strings.ReplaceAll(replaced, old, new)
+	return replaced
+}
diff --git a/pkg/cmd/cache/delete/delete_test.go b/pkg/cmd/cache/delete/delete_test.go
index 8660d693b..4cfa3ce26 100644
--- a/pkg/cmd/cache/delete/delete_test.go
+++ b/pkg/cmd/cache/delete/delete_test.go
@@ -58,11 +58,36 @@ func TestNewCmdDelete(t *testing.T) {
 			cli:      "--succeed-on-no-caches 123",
 			wantsErr: "--succeed-on-no-caches must be used in conjunction with --all",
 		},
+		{
+			name:     "key argument and delete all flag",
+			cli:      "cache-key --all",
+			wantsErr: "specify only one of cache id, cache key, or --all",
+		},
 		{
 			name:     "id argument and delete all flag",
 			cli:      "1 --all",
 			wantsErr: "specify only one of cache id, cache key, or --all",
 		},
+		{
+			name:  "key argument with ref",
+			cli:   "cache-key --ref refs/heads/main",
+			wants: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
+		},
+		{
+			name:     "ref flag without cache key",
+			cli:      "--ref refs/heads/main",
+			wantsErr: "must provide a cache key",
+		},
+		{
+			name:     "ref flag with cache id",
+			cli:      "123 --ref refs/heads/main",
+			wantsErr: "--ref cannot be used with cache ID",
+		},
+		{
+			name:     "ref flag with all flag",
+			cli:      "--all --ref refs/heads/main",
+			wantsErr: "--ref cannot be used with --all",
+		},
 	}
 
 	for _, tt := range tests {
@@ -89,6 +114,7 @@ func TestNewCmdDelete(t *testing.T) {
 			assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll)
 			assert.Equal(t, tt.wants.SucceedOnNoCaches, gotOpts.SucceedOnNoCaches)
 			assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier)
+			assert.Equal(t, tt.wants.Ref, gotOpts.Ref)
 		})
 	}
 }
@@ -209,7 +235,8 @@ func TestDeleteRun(t *testing.T) {
 					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
 						"key": []string{"a weird＿cache+key"},
 					}),
-					httpmock.StatusStringResponse(204, ""),
+					// The response is a JSON object but we don't need it here.
+					httpmock.StatusStringResponse(200, "{}"),
 				)
 			},
 			tty:        true,
@@ -263,6 +290,54 @@ func TestDeleteRun(t *testing.T) {
 			wantErr:    false,
 			wantStdout: "",
 		},
+		{
+			name: "deletes cache with ref tty",
+			opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
+			stubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
+						"key": []string{"cache-key"},
+						"ref": []string{"refs/heads/main"},
+					}),
+					// The response is a JSON object but we don't need it here.
+					httpmock.StatusStringResponse(200, "{}"),
+				)
+			},
+			tty:        true,
+			wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
+		},
+		{
+			name: "deletes cache with ref non-tty",
+			opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/main"},
+			stubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
+						"key": []string{"cache-key"},
+						"ref": []string{"refs/heads/main"},
+					}),
+					// The response is a JSON object but we don't need it here.
+					httpmock.StatusStringResponse(200, "{}"),
+				)
+			},
+			tty:        false,
+			wantStdout: "",
+		},
+		{
+			// As of now, the API returns HTTP 404 for invalid or non-existent refs.
+			name: "cache key exists but ref is invalid/not-found",
+			opts: DeleteOptions{Identifier: "existing-cache-key", Ref: "invalid-ref"},
+			stubs: func(reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
+						"key": []string{"existing-cache-key"},
+						"ref": []string{"invalid-ref"},
+					}),
+					httpmock.StatusStringResponse(404, ""),
+				)
+			},
+			wantErr:    true,
+			wantErrMsg: "X Could not find a cache matching existing-cache-key (with ref invalid-ref) in OWNER/REPO",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/pkg/cmd/extension/browse/browse_test.go b/pkg/cmd/extension/browse/browse_test.go
index 956ea0fc4..8120da4d3 100644
--- a/pkg/cmd/extension/browse/browse_test.go
+++ b/pkg/cmd/extension/browse/browse_test.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/cli/cli/v2/internal/config"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/cmd/repo/view"
@@ -125,7 +126,7 @@ func Test_getExtensionRepos(t *testing.T) {
 		}),
 	)
 
-	searcher := search.NewSearcher(client, "github.com")
+	searcher := search.NewSearcher(client, "github.com", &fd.DisabledDetectorMock{})
 	emMock := &extensions.ExtensionManagerMock{}
 	emMock.ListFunc = func() []extensions.Extension {
 		return []extensions.Extension{
diff --git a/pkg/cmd/issue/list/http_test.go b/pkg/cmd/issue/list/http_test.go
index a929746d1..747bd0a4b 100644
--- a/pkg/cmd/issue/list/http_test.go
+++ b/pkg/cmd/issue/list/http_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/cli/cli/v2/api"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
 	"github.com/cli/cli/v2/pkg/httpmock"
@@ -165,3 +166,51 @@ func TestIssueList_pagination(t *testing.T) {
 	assert.Equal(t, []string{"enhancement"}, getLabels(res.Issues[1]))
 	assert.Equal(t, []string{"user2"}, getAssignees(res.Issues[1]))
 }
+
+// TODO advancedIssueSearchCleanup
+// Remove this test once GHES 3.17 support ends.
+func TestSearchIssuesAndAdvancedSearch(t *testing.T) {
+	tests := []struct {
+		name           string
+		detector       fd.Detector
+		wantSearchType string
+	}{
+		{
+			name:           "advanced issue search not supported",
+			detector:       fd.AdvancedIssueSearchUnsupported(),
+			wantSearchType: "ISSUE",
+		},
+		{
+			name:           "advanced issue search supported as opt-in",
+			detector:       fd.AdvancedIssueSearchSupportedAsOptIn(),
+			wantSearchType: "ISSUE_ADVANCED",
+		},
+		{
+			name:           "advanced issue search supported as only backend",
+			detector:       fd.AdvancedIssueSearchSupportedAsOnlyBackend(),
+			wantSearchType: "ISSUE",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			defer reg.Verify(t)
+
+			reg.Register(
+				httpmock.GraphQL(`query IssueSearch\b`),
+				httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
+					assert.Equal(t, tt.wantSearchType, vars["type"])
+					// Since no repeated usage of special search qualifiers is possible
+					// with our current implementation, we can assert against the same
+					// query for both search backend (i.e. legacy and advanced issue search).
+					assert.Equal(t, "repo:OWNER/REPO state:open type:issue", vars["query"])
+				}))
+
+			httpClient := &http.Client{Transport: reg}
+			client := api.NewClientFromHTTP(httpClient)
+
+			searchIssues(client, tt.detector, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{State: "open"}, 30)
+		})
+	}
+}
diff --git a/pkg/cmd/issue/list/list_test.go b/pkg/cmd/issue/list/list_test.go
index 852f0a46b..83dc313ce 100644
--- a/pkg/cmd/issue/list/list_test.go
+++ b/pkg/cmd/issue/list/list_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/browser"
 	"github.com/cli/cli/v2/internal/config"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/run"
@@ -189,50 +190,80 @@ func TestIssueList_disabledIssues(t *testing.T) {
 	}
 }
 
+// TODO advancedIssueSearchCleanup
+// Simplify this test to only a single test case once GHES 3.17 support ends.
 func TestIssueList_web(t *testing.T) {
-	ios, _, stdout, stderr := iostreams.Test()
-	ios.SetStdoutTTY(true)
-	ios.SetStderrTTY(true)
-	browser := &browser.Stub{}
-
-	reg := &httpmock.Registry{}
-	defer reg.Verify(t)
-
-	_, cmdTeardown := run.Stub()
-	defer cmdTeardown(t)
-
-	err := listRun(&ListOptions{
-		IO:      ios,
-		Browser: browser,
-		HttpClient: func() (*http.Client, error) {
-			return &http.Client{Transport: reg}, nil
+	tests := []struct {
+		name     string
+		detector fd.Detector
+	}{
+		{
+			name:     "advanced issue search not supported",
+			detector: fd.AdvancedIssueSearchUnsupported(),
 		},
-		BaseRepo: func() (ghrepo.Interface, error) {
-			return ghrepo.New("OWNER", "REPO"), nil
+		{
+			name:     "advanced issue search supported as opt-in",
+			detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+		},
+		{
+			name:     "advanced issue search supported as only backend",
+			detector: fd.AdvancedIssueSearchSupportedAsOnlyBackend(),
 		},
-		WebMode:      true,
-		State:        "all",
-		Assignee:     "peter",
-		Author:       "john",
-		Labels:       []string{"bug", "docs"},
-		Mention:      "frank",
-		Milestone:    "v1.1",
-		LimitResults: 10,
-	})
-	if err != nil {
-		t.Errorf("error running command `issue list` with `--web` flag: %v", err)
 	}
 
-	assert.Equal(t, "", stdout.String())
-	assert.Equal(t, "Opening https://github.com/OWNER/REPO/issues in your browser.\n", stderr.String())
-	browser.Verify(t, "https://github.com/OWNER/REPO/issues?q=assignee%3Apeter+author%3Ajohn+label%3Abug+label%3Adocs+mentions%3Afrank+milestone%3Av1.1+type%3Aissue")
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ios, _, stdout, stderr := iostreams.Test()
+			ios.SetStdoutTTY(true)
+			ios.SetStderrTTY(true)
+			browser := &browser.Stub{}
+
+			reg := &httpmock.Registry{}
+			defer reg.Verify(t)
+
+			_, cmdTeardown := run.Stub()
+			defer cmdTeardown(t)
+
+			opts := &ListOptions{
+				IO:      ios,
+				Browser: browser,
+				HttpClient: func() (*http.Client, error) {
+					return &http.Client{Transport: reg}, nil
+				},
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+				Detector:     tt.detector,
+				WebMode:      true,
+				State:        "all",
+				Assignee:     "peter",
+				Author:       "john",
+				Labels:       []string{"bug", "docs"},
+				Mention:      "frank",
+				Milestone:    "v1.1",
+				LimitResults: 10,
+			}
+
+			err := listRun(opts)
+			require.NoError(t, err)
+
+			assert.Equal(t, "", stdout.String())
+			assert.Equal(t, "Opening https://github.com/OWNER/REPO/issues in your browser.\n", stderr.String())
+
+			// Since no repeated usage of special search qualifiers is possible
+			// with our current implementation, we can assert against the same
+			// URL for both search backend (i.e. legacy and advanced issue search).
+			browser.Verify(t, "https://github.com/OWNER/REPO/issues?q=assignee%3Apeter+author%3Ajohn+label%3Abug+label%3Adocs+mentions%3Afrank+milestone%3Av1.1+type%3Aissue")
+		})
+	}
 }
 
 func Test_issueList(t *testing.T) {
 	type args struct {
-		repo    ghrepo.Interface
-		filters prShared.FilterOptions
-		limit   int
+		detector fd.Detector
+		repo     ghrepo.Interface
+		filters  prShared.FilterOptions
+		limit    int
 	}
 	tests := []struct {
 		name      string
@@ -270,8 +301,11 @@ func Test_issueList(t *testing.T) {
 		{
 			name: "milestone by number",
 			args: args{
-				limit: 30,
-				repo:  ghrepo.New("OWNER", "REPO"),
+				// TODO advancedIssueSearchCleanup
+				// No need for feature detection once GHES 3.17 support ends.
+				detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+				limit:    30,
+				repo:     ghrepo.New("OWNER", "REPO"),
 				filters: prShared.FilterOptions{
 					Entity:    "issue",
 					State:     "open",
@@ -301,7 +335,7 @@ func Test_issueList(t *testing.T) {
 							"repo":  "REPO",
 							"limit": float64(30),
 							"query": "milestone:1.x repo:OWNER/REPO state:open type:issue",
-							"type":  "ISSUE",
+							"type":  "ISSUE_ADVANCED",
 						}, params)
 					}))
 			},
@@ -309,8 +343,11 @@ func Test_issueList(t *testing.T) {
 		{
 			name: "milestone by title",
 			args: args{
-				limit: 30,
-				repo:  ghrepo.New("OWNER", "REPO"),
+				// TODO advancedIssueSearchCleanup
+				// No need for feature detection once GHES 3.17 support ends.
+				detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+				limit:    30,
+				repo:     ghrepo.New("OWNER", "REPO"),
 				filters: prShared.FilterOptions{
 					Entity:    "issue",
 					State:     "open",
@@ -333,7 +370,7 @@ func Test_issueList(t *testing.T) {
 							"repo":  "REPO",
 							"limit": float64(30),
 							"query": "milestone:1.x repo:OWNER/REPO state:open type:issue",
-							"type":  "ISSUE",
+							"type":  "ISSUE_ADVANCED",
 						}, params)
 					}))
 			},
@@ -377,8 +414,11 @@ func Test_issueList(t *testing.T) {
 		{
 			name: "@me with search",
 			args: args{
-				limit: 30,
-				repo:  ghrepo.New("OWNER", "REPO"),
+				// TODO advancedIssueSearchCleanup
+				// No need for feature detection once GHES 3.17 support ends.
+				detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+				limit:    30,
+				repo:     ghrepo.New("OWNER", "REPO"),
 				filters: prShared.FilterOptions{
 					Entity:   "issue",
 					State:    "open",
@@ -404,7 +444,7 @@ func Test_issueList(t *testing.T) {
 							"repo":  "REPO",
 							"limit": float64(30),
 							"query": "auth bug assignee:@me author:@me mentions:@me repo:OWNER/REPO state:open type:issue",
-							"type":  "ISSUE",
+							"type":  "ISSUE_ADVANCED",
 						}, params)
 					}))
 			},
@@ -412,8 +452,11 @@ func Test_issueList(t *testing.T) {
 		{
 			name: "with labels",
 			args: args{
-				limit: 30,
-				repo:  ghrepo.New("OWNER", "REPO"),
+				// TODO advancedIssueSearchCleanup
+				// No need for feature detection once GHES 3.17 support ends.
+				detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+				limit:    30,
+				repo:     ghrepo.New("OWNER", "REPO"),
 				filters: prShared.FilterOptions{
 					Entity: "issue",
 					State:  "open",
@@ -436,7 +479,7 @@ func Test_issueList(t *testing.T) {
 							"repo":  "REPO",
 							"limit": float64(30),
 							"query": `label:"one world" label:hello repo:OWNER/REPO state:open type:issue`,
-							"type":  "ISSUE",
+							"type":  "ISSUE_ADVANCED",
 						}, params)
 					}))
 			},
@@ -450,7 +493,7 @@ func Test_issueList(t *testing.T) {
 				tt.httpStubs(httpreg)
 			}
 			client := &http.Client{Transport: httpreg}
-			_, err := issueList(client, tt.args.repo, tt.args.filters, tt.args.limit)
+			_, err := issueList(client, tt.args.detector, tt.args.repo, tt.args.filters, tt.args.limit)
 			if tt.wantErr {
 				assert.Error(t, err)
 			} else {
@@ -507,6 +550,7 @@ func TestIssueList_withProjectItems(t *testing.T) {
 	client := &http.Client{Transport: reg}
 	issuesAndTotalCount, err := issueList(
 		client,
+		nil,
 		ghrepo.New("OWNER", "REPO"),
 		prShared.FilterOptions{
 			Entity: "issue",
@@ -572,7 +616,7 @@ func TestIssueList_Search_withProjectItems(t *testing.T) {
 			require.Equal(t, map[string]interface{}{
 				"owner": "OWNER",
 				"repo":  "REPO",
-				"type":  "ISSUE",
+				"type":  "ISSUE_ADVANCED",
 				"limit": float64(30),
 				"query": "just used to force the search API branch repo:OWNER/REPO type:issue",
 			}, params)
@@ -581,6 +625,9 @@ func TestIssueList_Search_withProjectItems(t *testing.T) {
 	client := &http.Client{Transport: reg}
 	issuesAndTotalCount, err := issueList(
 		client,
+		// TODO advancedIssueSearchCleanup
+		// No need for feature detection once GHES 3.17 support ends.
+		fd.AdvancedIssueSearchSupportedAsOptIn(),
 		ghrepo.New("OWNER", "REPO"),
 		prShared.FilterOptions{
 			Entity: "issue",
diff --git a/pkg/cmd/pr/list/http_test.go b/pkg/cmd/pr/list/http_test.go
index 1aa16ae1d..5f08498f7 100644
--- a/pkg/cmd/pr/list/http_test.go
+++ b/pkg/cmd/pr/list/http_test.go
@@ -5,16 +5,19 @@ import (
 	"reflect"
 	"testing"
 
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
 	"github.com/cli/cli/v2/pkg/httpmock"
+	"github.com/stretchr/testify/assert"
 )
 
 func Test_ListPullRequests(t *testing.T) {
 	type args struct {
-		repo    ghrepo.Interface
-		filters prShared.FilterOptions
-		limit   int
+		detector fd.Detector
+		repo     ghrepo.Interface
+		filters  prShared.FilterOptions
+		limit    int
 	}
 	tests := []struct {
 		name     string
@@ -75,8 +78,11 @@ func Test_ListPullRequests(t *testing.T) {
 		{
 			name: "with labels",
 			args: args{
-				repo:  ghrepo.New("OWNER", "REPO"),
-				limit: 30,
+				// TODO advancedIssueSearchCleanup
+				// No need for feature detection once GHES 3.17 support ends.
+				detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+				repo:     ghrepo.New("OWNER", "REPO"),
+				limit:    30,
 				filters: prShared.FilterOptions{
 					State:  "open",
 					Labels: []string{"hello", "one world"},
@@ -88,6 +94,7 @@ func Test_ListPullRequests(t *testing.T) {
 					httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
 						want := map[string]interface{}{
 							"q":     `label:"one world" label:hello repo:OWNER/REPO state:open type:pr`,
+							"type":  "ISSUE_ADVANCED",
 							"limit": float64(30),
 						}
 						if !reflect.DeepEqual(vars, want) {
@@ -99,8 +106,11 @@ func Test_ListPullRequests(t *testing.T) {
 		{
 			name: "with author",
 			args: args{
-				repo:  ghrepo.New("OWNER", "REPO"),
-				limit: 30,
+				// TODO advancedIssueSearchCleanup
+				// No need for feature detection once GHES 3.17 support ends.
+				detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+				repo:     ghrepo.New("OWNER", "REPO"),
+				limit:    30,
 				filters: prShared.FilterOptions{
 					State:  "open",
 					Author: "monalisa",
@@ -112,6 +122,7 @@ func Test_ListPullRequests(t *testing.T) {
 					httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
 						want := map[string]interface{}{
 							"q":     "author:monalisa repo:OWNER/REPO state:open type:pr",
+							"type":  "ISSUE_ADVANCED",
 							"limit": float64(30),
 						}
 						if !reflect.DeepEqual(vars, want) {
@@ -123,8 +134,11 @@ func Test_ListPullRequests(t *testing.T) {
 		{
 			name: "with search",
 			args: args{
-				repo:  ghrepo.New("OWNER", "REPO"),
-				limit: 30,
+				// TODO advancedIssueSearchCleanup
+				// No need for feature detection once GHES 3.17 support ends.
+				detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+				repo:     ghrepo.New("OWNER", "REPO"),
+				limit:    30,
 				filters: prShared.FilterOptions{
 					State:  "open",
 					Search: "one world in:title",
@@ -136,6 +150,7 @@ func Test_ListPullRequests(t *testing.T) {
 					httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
 						want := map[string]interface{}{
 							"q":     "one world in:title repo:OWNER/REPO state:open type:pr",
+							"type":  "ISSUE_ADVANCED",
 							"limit": float64(30),
 						}
 						if !reflect.DeepEqual(vars, want) {
@@ -153,7 +168,7 @@ func Test_ListPullRequests(t *testing.T) {
 			}
 			httpClient := &http.Client{Transport: reg}
 
-			_, err := listPullRequests(httpClient, tt.args.repo, tt.args.filters, tt.args.limit)
+			_, err := listPullRequests(httpClient, tt.args.detector, tt.args.repo, tt.args.filters, tt.args.limit)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("ListPullRequests() error = %v, wantErr %v", err, tt.wantErr)
 				return
@@ -161,3 +176,51 @@ func Test_ListPullRequests(t *testing.T) {
 		})
 	}
 }
+
+// TODO advancedIssueSearchCleanup
+// Remove this test once GHES 3.17 support ends.
+func TestSearchPullRequestsAndAdvancedSearch(t *testing.T) {
+	tests := []struct {
+		name           string
+		detector       fd.Detector
+		wantSearchType string
+	}{
+		{
+			name:           "advanced issue search not supported",
+			detector:       fd.AdvancedIssueSearchUnsupported(),
+			wantSearchType: "ISSUE",
+		},
+		{
+			name:           "advanced issue search supported as opt-in",
+			detector:       fd.AdvancedIssueSearchSupportedAsOptIn(),
+			wantSearchType: "ISSUE_ADVANCED",
+		},
+		{
+			name:           "advanced issue search supported as only backend",
+			detector:       fd.AdvancedIssueSearchSupportedAsOnlyBackend(),
+			wantSearchType: "ISSUE",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			defer reg.Verify(t)
+
+			reg.Register(
+				httpmock.GraphQL(`query PullRequestSearch\b`),
+				httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
+					assert.Equal(t, tt.wantSearchType, vars["type"])
+
+					// Since no repeated usage of special search qualifiers is possible
+					// with our current implementation, we can assert against the same
+					// query for both search backend (i.e. legacy and advanced issue search).
+					assert.Equal(t, "repo:OWNER/REPO state:open type:pr", vars["q"])
+				}))
+
+			httpClient := &http.Client{Transport: reg}
+
+			searchPullRequests(httpClient, tt.detector, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{State: "open"}, 30)
+		})
+	}
+}
diff --git a/pkg/cmd/pr/list/list_test.go b/pkg/cmd/pr/list/list_test.go
index ecd0326b5..4953b8888 100644
--- a/pkg/cmd/pr/list/list_test.go
+++ b/pkg/cmd/pr/list/list_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/browser"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/run"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
@@ -23,7 +24,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
+func runCommand(rt http.RoundTripper, detector fd.Detector, isTTY bool, cli string) (*test.CmdOut, error) {
 	ios, _, stdout, stderr := iostreams.Test()
 	ios.SetStdoutTTY(isTTY)
 	ios.SetStdinTTY(isTTY)
@@ -47,6 +48,7 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err
 
 	cmd := NewCmdList(factory, func(opts *ListOptions) error {
 		opts.Now = fakeNow
+		opts.Detector = detector
 		return listRun(opts)
 	})
 
@@ -78,7 +80,7 @@ func TestPRList(t *testing.T) {
 
 	http.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json"))
 
-	output, err := runCommand(http, true, "")
+	output, err := runCommand(http, nil, true, "")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -101,7 +103,7 @@ func TestPRList_nontty(t *testing.T) {
 
 	http.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json"))
 
-	output, err := runCommand(http, false, "")
+	output, err := runCommand(http, nil, false, "")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -124,7 +126,7 @@ func TestPRList_filtering(t *testing.T) {
 			assert.Equal(t, []interface{}{"OPEN", "CLOSED", "MERGED"}, params["state"].([]interface{}))
 		}))
 
-	output, err := runCommand(http, true, `-s all`)
+	output, err := runCommand(http, nil, true, `-s all`)
 	assert.Error(t, err)
 
 	assert.Equal(t, "", output.String())
@@ -139,7 +141,7 @@ func TestPRList_filteringRemoveDuplicate(t *testing.T) {
 		httpmock.GraphQL(`query PullRequestList\b`),
 		httpmock.FileResponse("./fixtures/prListWithDuplicates.json"))
 
-	output, err := runCommand(http, true, "")
+	output, err := runCommand(http, nil, true, "")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -162,7 +164,7 @@ func TestPRList_filteringClosed(t *testing.T) {
 			assert.Equal(t, []interface{}{"CLOSED", "MERGED"}, params["state"].([]interface{}))
 		}))
 
-	_, err := runCommand(http, true, `-s closed`)
+	_, err := runCommand(http, nil, true, `-s closed`)
 	assert.Error(t, err)
 }
 
@@ -176,7 +178,7 @@ func TestPRList_filteringHeadBranch(t *testing.T) {
 			assert.Equal(t, interface{}("bug-fix"), params["headBranch"])
 		}))
 
-	_, err := runCommand(http, true, `-H bug-fix`)
+	_, err := runCommand(http, nil, true, `-H bug-fix`)
 	assert.Error(t, err)
 }
 
@@ -190,7 +192,9 @@ func TestPRList_filteringAssignee(t *testing.T) {
 			assert.Equal(t, `assignee:hubot base:develop is:merged label:"needs tests" repo:OWNER/REPO type:pr`, params["q"].(string))
 		}))
 
-	_, err := runCommand(http, true, `-s merged -l "needs tests" -a hubot -B develop`)
+	// TODO advancedIssueSearchCleanup
+	// No need for feature detection once GHES 3.17 support ends.
+	_, err := runCommand(http, fd.AdvancedIssueSearchSupportedAsOptIn(), true, `-s merged -l "needs tests" -a hubot -B develop`)
 	assert.Error(t, err)
 }
 
@@ -223,7 +227,9 @@ func TestPRList_filteringDraft(t *testing.T) {
 					assert.Equal(t, test.expectedQuery, params["q"].(string))
 				}))
 
-			_, err := runCommand(http, true, test.cli)
+			// TODO advancedIssueSearchCleanup
+			// No need for feature detection once GHES 3.17 support ends.
+			_, err := runCommand(http, fd.AdvancedIssueSearchSupportedAsOptIn(), true, test.cli)
 			assert.Error(t, err)
 		})
 	}
@@ -268,7 +274,9 @@ func TestPRList_filteringAuthor(t *testing.T) {
 					assert.Equal(t, test.expectedQuery, params["q"].(string))
 				}))
 
-			_, err := runCommand(http, true, test.cli)
+			// TODO advancedIssueSearchCleanup
+			// No need for feature detection once GHES 3.17 support ends.
+			_, err := runCommand(http, fd.AdvancedIssueSearchSupportedAsOptIn(), true, test.cli)
 			assert.Error(t, err)
 		})
 	}
@@ -277,7 +285,7 @@ func TestPRList_filteringAuthor(t *testing.T) {
 func TestPRList_withInvalidLimitFlag(t *testing.T) {
 	http := initFakeHTTP()
 	defer http.Verify(t)
-	_, err := runCommand(http, true, `--limit=0`)
+	_, err := runCommand(http, nil, true, `--limit=0`)
 	assert.EqualError(t, err, "invalid value for --limit: 0")
 }
 
@@ -312,7 +320,7 @@ func TestPRList_web(t *testing.T) {
 			_, cmdTeardown := run.Stub()
 			defer cmdTeardown(t)
 
-			output, err := runCommand(http, true, "--web "+test.cli)
+			output, err := runCommand(http, nil, true, "--web "+test.cli)
 			if err != nil {
 				t.Errorf("error running command `pr list` with `--web` flag: %v", err)
 			}
@@ -370,6 +378,7 @@ func TestPRList_withProjectItems(t *testing.T) {
 	client := &http.Client{Transport: reg}
 	prsAndTotalCount, err := listPullRequests(
 		client,
+		nil,
 		ghrepo.New("OWNER", "REPO"),
 		prShared.FilterOptions{
 			Entity: "pr",
@@ -433,12 +442,16 @@ func TestPRList_Search_withProjectItems(t *testing.T) {
 			require.Equal(t, map[string]interface{}{
 				"limit": float64(30),
 				"q":     "just used to force the search API branch repo:OWNER/REPO state:open type:pr",
+				"type":  "ISSUE_ADVANCED",
 			}, params)
 		}))
 
 	client := &http.Client{Transport: reg}
 	prsAndTotalCount, err := listPullRequests(
 		client,
+		// TODO advancedIssueSearchCleanup
+		// No need for feature detection once GHES 3.17 support ends.
+		fd.AdvancedIssueSearchSupportedAsOptIn(),
 		ghrepo.New("OWNER", "REPO"),
 		prShared.FilterOptions{
 			Entity: "pr",
diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go
index 0f6da5a6e..3f9ba6f50 100644
--- a/pkg/cmd/pr/shared/finder_test.go
+++ b/pkg/cmd/pr/shared/finder_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/httpmock"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -20,6 +21,7 @@ func TestParseURL(t *testing.T) {
 		arg      string
 		wantRepo ghrepo.Interface
 		wantNum  int
+		wantRest string
 		wantErr  string
 	}{
 		{
@@ -34,15 +36,46 @@ func TestParseURL(t *testing.T) {
 			wantRepo: ghrepo.NewWithHost("owner", "repo", "example.com"),
 			wantNum:  123,
 		},
+		{
+			name:     "valid HTTP URL with rest",
+			arg:      "http://example.com/owner/repo/pull/123/foo/bar",
+			wantRepo: ghrepo.NewWithHost("owner", "repo", "example.com"),
+			wantNum:  123,
+			wantRest: "/foo/bar",
+		},
+		{
+			name:     "valid HTTP URL with .patch as rest",
+			arg:      "http://example.com/owner/repo/pull/123.patch",
+			wantRepo: ghrepo.NewWithHost("owner", "repo", "example.com"),
+			wantNum:  123,
+			wantRest: ".patch",
+		},
+		{
+			name:     "valid HTTP URL with a trailing slash",
+			arg:      "http://example.com/owner/repo/pull/123/",
+			wantRepo: ghrepo.NewWithHost("owner", "repo", "example.com"),
+			wantNum:  123,
+			wantRest: "/",
+		},
 		{
 			name:    "empty URL",
 			wantErr: "invalid URL: \"\"",
 		},
+		{
+			name:    "no scheme",
+			arg:     "github.com/owner/repo/pull/123",
+			wantErr: "invalid scheme: ",
+		},
 		{
 			name:    "invalid scheme",
 			arg:     "ftp://github.com/owner/repo/pull/123",
 			wantErr: "invalid scheme: ftp",
 		},
+		{
+			name:    "no hostname",
+			arg:     "/owner/repo/pull/123",
+			wantErr: "invalid scheme: ",
+		},
 		{
 			name:    "incorrect path",
 			arg:     "https://github.com/owner/repo/issues/123",
@@ -62,7 +95,7 @@ func TestParseURL(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			repo, num, err := ParseURL(tt.arg)
+			repo, num, rest, err := ParseURL(tt.arg)
 
 			if tt.wantErr != "" {
 				require.Error(t, err)
@@ -72,19 +105,96 @@ func TestParseURL(t *testing.T) {
 
 			require.NoError(t, err)
 			require.Equal(t, tt.wantNum, num)
+			require.Equal(t, tt.wantRest, rest)
 			require.NotNil(t, repo)
 			require.True(t, ghrepo.IsSame(tt.wantRepo, repo))
 		})
 	}
 }
 
+func TestParseFullReference(t *testing.T) {
+	tests := []struct {
+		name       string
+		arg        string
+		wantRepo   ghrepo.Interface
+		wantNumber int
+		wantErr    string
+	}{
+		{
+			name:    "number",
+			arg:     "123",
+			wantErr: `invalid reference: "123"`,
+		},
+		{
+			name:    "number with hash",
+			arg:     "#123",
+			wantErr: `invalid reference: "#123"`,
+		},
+		{
+			name:       "full form",
+			arg:        "OWNER/REPO#123",
+			wantNumber: 123,
+			wantRepo:   ghrepo.New("OWNER", "REPO"),
+		},
+		{
+			name:    "empty",
+			wantErr: "empty reference",
+		},
+		{
+			name:    "invalid full form, without hash",
+			arg:     "OWNER/REPO123",
+			wantErr: `invalid reference: "OWNER/REPO123"`,
+		},
+		{
+			name:    "invalid full form, empty owner and repo",
+			arg:     "/#123",
+			wantErr: `invalid reference: "/#123"`,
+		},
+		{
+			name:    "invalid full form, without owner",
+			arg:     "REPO#123",
+			wantErr: `invalid reference: "REPO#123"`,
+		},
+		{
+			name:    "invalid full form, without repo",
+			arg:     "OWNER/#123",
+			wantErr: `invalid reference: "OWNER/#123"`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			repo, number, err := ParseFullReference(tt.arg)
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+				assert.Nil(t, repo)
+				assert.Zero(t, number)
+				return
+			}
+
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantNumber, number)
+
+			if tt.wantRepo != nil {
+				require.NotNil(t, repo)
+				assert.True(t, ghrepo.IsSame(tt.wantRepo, repo))
+			} else {
+				assert.Nil(t, repo)
+			}
+		})
+	}
+}
+
 type args struct {
 	baseRepoFn      func() (ghrepo.Interface, error)
 	branchFn        func() (string, error)
 	gitConfigClient stubGitConfigClient
+	progress        *stubProgressIndicator
 	selector        string
 	fields          []string
 	baseBranch      string
+	disableProgress bool
 }
 
 func TestFind(t *testing.T) {
@@ -120,12 +230,13 @@ func TestFind(t *testing.T) {
 	}
 
 	tests := []struct {
-		name     string
-		args     args
-		httpStub func(*httpmock.Registry)
-		wantPR   int
-		wantRepo string
-		wantErr  bool
+		name            string
+		args            args
+		httpStub        func(*httpmock.Registry)
+		wantUseProgress bool
+		wantPR          int
+		wantRepo        string
+		wantErr         bool
 	}{
 		{
 			name: "number argument",
@@ -716,6 +827,51 @@ func TestFind(t *testing.T) {
 			wantPR:   13,
 			wantRepo: "https://github.com/OWNER/REPO",
 		},
+		{
+			name: "number argument, with non nil-progress indicator",
+			args: args{
+				selector:   "13",
+				fields:     []string{"id", "number"},
+				baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
+				branchFn: func() (string, error) {
+					return "blueberries", nil
+				},
+				progress: &stubProgressIndicator{},
+			},
+			httpStub: func(r *httpmock.Registry) {
+				r.Register(
+					httpmock.GraphQL(`query PullRequestByNumber\b`),
+					httpmock.StringResponse(`{"data":{"repository":{
+						"pullRequest":{"number":13}
+					}}}`))
+			},
+			wantPR:          13,
+			wantRepo:        "https://github.com/ORIGINOWNER/REPO",
+			wantUseProgress: true,
+		},
+		{
+			name: "number argument, with non-nil progress indicator and DisableProgress set",
+			args: args{
+				selector:   "13",
+				fields:     []string{"id", "number"},
+				baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
+				branchFn: func() (string, error) {
+					return "blueberries", nil
+				},
+				progress:        &stubProgressIndicator{},
+				disableProgress: true,
+			},
+			httpStub: func(r *httpmock.Registry) {
+				r.Register(
+					httpmock.GraphQL(`query PullRequestByNumber\b`),
+					httpmock.StringResponse(`{"data":{"repository":{
+						"pullRequest":{"number":13}
+					}}}`))
+			},
+			wantPR:          13,
+			wantRepo:        "https://github.com/ORIGINOWNER/REPO",
+			wantUseProgress: false,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -739,11 +895,25 @@ func TestFind(t *testing.T) {
 				}, nil),
 			}
 
+			if tt.args.progress != nil {
+				f.progress = tt.args.progress
+			}
+
 			pr, repo, err := f.Find(FindOptions{
-				Selector:   tt.args.selector,
-				Fields:     tt.args.fields,
-				BaseBranch: tt.args.baseBranch,
+				Selector:        tt.args.selector,
+				Fields:          tt.args.fields,
+				BaseBranch:      tt.args.baseBranch,
+				DisableProgress: tt.args.disableProgress,
 			})
+
+			if tt.args.progress != nil {
+				if tt.args.progress.startCalled != tt.wantUseProgress {
+					t.Errorf("progress was (not) used as expected")
+				} else if tt.args.progress.startCalled != tt.args.progress.stopCalled {
+					t.Errorf("progress was started but not stopped")
+				}
+			}
+
 			if (err != nil) != tt.wantErr {
 				t.Errorf("Find() error = %v, wantErr %v", err, tt.wantErr)
 				return
@@ -839,3 +1009,16 @@ func (s stubGitConfigClient) PushRevision(ctx context.Context, branchName string
 	}
 	return s.pushRevisionFn(ctx, branchName)
 }
+
+type stubProgressIndicator struct {
+	startCalled bool
+	stopCalled  bool
+}
+
+func (s *stubProgressIndicator) StartProgressIndicator() {
+	s.startCalled = true
+}
+
+func (s *stubProgressIndicator) StopProgressIndicator() {
+	s.stopCalled = true
+}
diff --git a/pkg/cmd/pr/shared/params_test.go b/pkg/cmd/pr/shared/params_test.go
index 3c95a9a5d..024965630 100644
--- a/pkg/cmd/pr/shared/params_test.go
+++ b/pkg/cmd/pr/shared/params_test.go
@@ -19,8 +19,9 @@ func Test_listURLWithQuery(t *testing.T) {
 	falseBool := false
 
 	type args struct {
-		listURL string
-		options FilterOptions
+		listURL                   string
+		options                   FilterOptions
+		advancedIssueSearchSyntax bool
 	}
 
 	tests := []struct {
@@ -41,6 +42,19 @@ func Test_listURLWithQuery(t *testing.T) {
 			want:    "https://example.com/path?a=b&q=state%3Aopen+type%3Aissue",
 			wantErr: false,
 		},
+		{
+			name: "blank, advanced search",
+			args: args{
+				listURL: "https://example.com/path?a=b",
+				options: FilterOptions{
+					Entity: "issue",
+					State:  "open",
+				},
+				advancedIssueSearchSyntax: true,
+			},
+			want:    "https://example.com/path?a=b&q=state%3Aopen+type%3Aissue",
+			wantErr: false,
+		},
 		{
 			name: "draft",
 			args: args{
@@ -54,6 +68,20 @@ func Test_listURLWithQuery(t *testing.T) {
 			want:    "https://example.com/path?q=draft%3Atrue+state%3Aopen+type%3Apr",
 			wantErr: false,
 		},
+		{
+			name: "draft, advanced search",
+			args: args{
+				listURL: "https://example.com/path",
+				options: FilterOptions{
+					Entity: "pr",
+					State:  "open",
+					Draft:  &trueBool,
+				},
+				advancedIssueSearchSyntax: true,
+			},
+			want:    "https://example.com/path?q=draft%3Atrue+state%3Aopen+type%3Apr",
+			wantErr: false,
+		},
 		{
 			name: "non-draft",
 			args: args{
@@ -67,6 +95,20 @@ func Test_listURLWithQuery(t *testing.T) {
 			want:    "https://example.com/path?q=draft%3Afalse+state%3Aopen+type%3Apr",
 			wantErr: false,
 		},
+		{
+			name: "non-draft, advanced search",
+			args: args{
+				listURL: "https://example.com/path",
+				options: FilterOptions{
+					Entity: "pr",
+					State:  "open",
+					Draft:  &falseBool,
+				},
+				advancedIssueSearchSyntax: true,
+			},
+			want:    "https://example.com/path?q=draft%3Afalse+state%3Aopen+type%3Apr",
+			wantErr: false,
+		},
 		{
 			name: "all",
 			args: args{
@@ -84,6 +126,24 @@ func Test_listURLWithQuery(t *testing.T) {
 			want:    "https://example.com/path?q=assignee%3Abo+author%3Aka+base%3Atrunk+head%3Abug-fix+mentions%3Anu+state%3Aopen+type%3Aissue",
 			wantErr: false,
 		},
+		{
+			name: "all, advanced search",
+			args: args{
+				listURL: "https://example.com/path",
+				options: FilterOptions{
+					Entity:     "issue",
+					State:      "open",
+					Assignee:   "bo",
+					Author:     "ka",
+					BaseBranch: "trunk",
+					HeadBranch: "bug-fix",
+					Mention:    "nu",
+				},
+				advancedIssueSearchSyntax: true,
+			},
+			want:    "https://example.com/path?q=assignee%3Abo+author%3Aka+base%3Atrunk+head%3Abug-fix+mentions%3Anu+state%3Aopen+type%3Aissue",
+			wantErr: false,
+		},
 		{
 			name: "spaces in values",
 			args: args{
@@ -98,10 +158,25 @@ func Test_listURLWithQuery(t *testing.T) {
 			want:    "https://example.com/path?q=label%3A%22help+wanted%22+label%3Adocs+milestone%3A%22Codename+%5C%22What+Was+Missing%5C%22%22+state%3Aopen+type%3Apr",
 			wantErr: false,
 		},
+		{
+			name: "spaces in values, advanced search",
+			args: args{
+				listURL: "https://example.com/path",
+				options: FilterOptions{
+					Entity:    "pr",
+					State:     "open",
+					Labels:    []string{"docs", "help wanted"},
+					Milestone: `Codename "What Was Missing"`,
+				},
+				advancedIssueSearchSyntax: true,
+			},
+			want:    "https://example.com/path?q=label%3A%22help+wanted%22+label%3Adocs+milestone%3A%22Codename+%5C%22What+Was+Missing%5C%22%22+state%3Aopen+type%3Apr",
+			wantErr: false,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := ListURLWithQuery(tt.args.listURL, tt.args.options)
+			got, err := ListURLWithQuery(tt.args.listURL, tt.args.options, tt.args.advancedIssueSearchSyntax)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("listURLWithQuery() error = %v, wantErr %v", err, tt.wantErr)
 				return
diff --git a/pkg/cmd/release/verify-asset/verify_asset_test.go b/pkg/cmd/release/verify-asset/verify_asset_test.go
index 732de9fd2..530f478ed 100644
--- a/pkg/cmd/release/verify-asset/verify_asset_test.go
+++ b/pkg/cmd/release/verify-asset/verify_asset_test.go
@@ -123,6 +123,49 @@ func Test_verifyAssetRun_Success(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func Test_verifyAssetRun_SuccessNoTagArg(t *testing.T) {
+	ios, _, _, _ := iostreams.Test()
+	tagName := "v6"
+
+	fakeHTTP := &httpmock.Registry{}
+	defer fakeHTTP.Verify(t)
+	fakeSHA := "1234567890abcdef1234567890abcdef12345678"
+	shared.StubFetchRefSHA(t, fakeHTTP, "OWNER", "REPO", tagName, fakeSHA)
+	shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", "", `{
+				"tag_name": "v6",
+				"draft": false,
+				"url": "https://api.github.com/repos/OWNER/REPO/releases/23456"
+			}`)
+
+	baseRepo, err := ghrepo.FromFullName("OWNER/REPO")
+	require.NoError(t, err)
+	result := &verification.AttestationProcessingResult{
+		Attestation: &api.Attestation{
+			Bundle:    data.GitHubReleaseBundle(t),
+			BundleURL: "https://example.com",
+		},
+		VerificationResult: nil,
+	}
+
+	releaseAssetPath := test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip")
+
+	cfg := &VerifyAssetConfig{
+		Opts: &VerifyAssetOptions{
+			AssetFilePath: releaseAssetPath,
+			TagName:       "", // No tag argument provided
+			BaseRepo:      baseRepo,
+			Exporter:      nil,
+		},
+		IO:          ios,
+		HttpClient:  &http.Client{Transport: fakeHTTP},
+		AttClient:   api.NewTestClient(),
+		AttVerifier: shared.NewMockVerifier(result),
+	}
+
+	err = verifyAssetRun(cfg)
+	require.NoError(t, err)
+}
+
 func Test_verifyAssetRun_FailedNoAttestations(t *testing.T) {
 	ios, _, _, _ := iostreams.Test()
 	tagName := "v1"
diff --git a/pkg/cmd/search/code/code_test.go b/pkg/cmd/search/code/code_test.go
index efb5f4b57..471a9d0cb 100644
--- a/pkg/cmd/search/code/code_test.go
+++ b/pkg/cmd/search/code/code_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/cli/cli/v2/internal/browser"
 	"github.com/cli/cli/v2/internal/config"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/gh"
 	ghmock "github.com/cli/cli/v2/internal/gh/mock"
 	"github.com/cli/cli/v2/pkg/cmdutil"
@@ -336,7 +337,7 @@ func TestCodeRun(t *testing.T) {
 						Extension: "go",
 					},
 				},
-				Searcher: search.NewSearcher(nil, "github.com"),
+				Searcher: search.NewSearcher(nil, "github.com", &fd.DisabledDetectorMock{}),
 				WebMode:  true,
 			},
 			wantBrowse: "https://github.com/search?q=map+path%3Atesting.go&type=code",
@@ -354,7 +355,7 @@ func TestCodeRun(t *testing.T) {
 						Extension: ".cpp",
 					},
 				},
-				Searcher: search.NewSearcher(nil, "github.com"),
+				Searcher: search.NewSearcher(nil, "github.com", &fd.DisabledDetectorMock{}),
 				WebMode:  true,
 			},
 			wantBrowse: "https://github.com/search?q=map+path%3Atesting.cpp&type=code",
@@ -381,7 +382,7 @@ func TestCodeRun(t *testing.T) {
 						Extension: "go",
 					},
 				},
-				Searcher: search.NewSearcher(nil, "example.com"),
+				Searcher: search.NewSearcher(nil, "example.com", &fd.DisabledDetectorMock{}),
 				WebMode:  true,
 			},
 			wantBrowse: "https://example.com/search?q=map+extension%3Ago+filename%3Atesting&type=code",
diff --git a/pkg/cmdutil/json_flags_test.go b/pkg/cmdutil/json_flags_test.go
index 63c63aa00..ee089960b 100644
--- a/pkg/cmdutil/json_flags_test.go
+++ b/pkg/cmdutil/json_flags_test.go
@@ -119,6 +119,44 @@ func TestAddJSONFlags(t *testing.T) {
 	}
 }
 
+func TestAddJSONFlagsWithoutShorthand(t *testing.T) {
+	tests := []struct {
+		name      string
+		setFlags  func(cmd *cobra.Command)
+		wantFlags map[string]string
+	}{
+		{
+			name: "no conflicting flags",
+			setFlags: func(cmd *cobra.Command) {
+				cmd.Flags().StringP("web", "w", "", "")
+				cmd.Flags().StringP("token", "t", "", "")
+			},
+			wantFlags: map[string]string{
+				"web":      "w",
+				"token":    "t",
+				"jq":       "",
+				"template": "",
+				"json":     "",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			cmd := &cobra.Command{Run: func(*cobra.Command, []string) {}}
+			tt.setFlags(cmd)
+
+			AddJSONFlagsWithoutShorthand(cmd, nil, []string{})
+
+			for f, shorthand := range tt.wantFlags {
+				flag := cmd.Flags().Lookup(f)
+				require.NotNil(t, flag)
+				require.Equal(t, shorthand, flag.Shorthand)
+			}
+		})
+	}
+}
+
 // TestAddJSONFlagsSetsAnnotations asserts that `AddJSONFlags` function adds the
 // appropriate annotation to the command, which could later be used by doc
 // generator functions.
diff --git a/pkg/search/query_test.go b/pkg/search/query_test.go
index ddec211cb..7c20e5348 100644
--- a/pkg/search/query_test.go
+++ b/pkg/search/query_test.go
@@ -8,7 +8,7 @@ import (
 
 var trueBool = true
 
-func TestQueryString(t *testing.T) {
+func TestStandardSearchString(t *testing.T) {
 	tests := []struct {
 		name  string
 		query Query
@@ -73,7 +73,109 @@ func TestQueryString(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			assert.Equal(t, tt.out, tt.query.String())
+			assert.Equal(t, tt.out, tt.query.StandardSearchString())
+		})
+	}
+}
+
+func TestAdvancedIssueSearchString(t *testing.T) {
+	tests := []struct {
+		name  string
+		query Query
+		out   string
+	}{
+		{
+			name: "quotes keywords",
+			query: Query{
+				Keywords: []string{"quote keywords"},
+			},
+			out: `"quote keywords"`,
+		},
+		{
+			name: "quotes keywords that are qualifiers",
+			query: Query{
+				Keywords: []string{"quote:keywords", "quote:multiword keywords"},
+			},
+			out: `quote:keywords quote:"multiword keywords"`,
+		},
+		{
+			name: "quotes qualifiers",
+			query: Query{
+				Qualifiers: Qualifiers{
+					Label: []string{"quote qualifier"},
+				},
+			},
+			out: `label:"quote qualifier"`,
+		},
+		{
+			name: "unused qualifiers should not appear in query",
+			query: Query{
+				Keywords: []string{"keyword"},
+				Qualifiers: Qualifiers{
+					Label: []string{"foo", "bar"},
+				},
+			},
+			out: `keyword label:bar label:foo`,
+		},
+		{
+			name: "special qualifiers when used once",
+			query: Query{
+				Keywords: []string{"keyword"},
+				Qualifiers: Qualifiers{
+					Repo: []string{"foo/bar"},
+					Is:   []string{"private"},
+					User: []string{"johndoe"},
+					In:   []string{"title"},
+				},
+			},
+			out: `keyword in:title is:private repo:foo/bar user:johndoe`,
+		},
+		{
+			name: "special qualifiers are OR-ed when used multiple times",
+			query: Query{
+				Keywords: []string{"keyword"},
+				Qualifiers: Qualifiers{
+					Repo: []string{"foo/bar", "foo/baz"},
+					Is:   []string{"private", "public", "issue", "pr", "open", "closed", "locked", "unlocked", "merged", "unmerged", "blocked", "blocking", "foo"}, // "foo" is to ensure only "public" and "private" are grouped
+					User: []string{"johndoe", "janedoe"},
+					In:   []string{"title", "body", "comments", "foo"}, // "foo" is to ensure only "title", "body", and "comments" are grouped
+				},
+			},
+			out: `keyword (in:body OR in:comments OR in:title) in:foo (is:blocked OR is:blocking) (is:closed OR is:open) (is:issue OR is:pr) (is:locked OR is:unlocked) (is:merged OR is:unmerged) (is:private OR is:public) is:foo (repo:foo/bar OR repo:foo/baz) (user:janedoe OR user:johndoe)`,
+		},
+		{
+			// Since this is a general purpose package, we can't assume with know all
+			// use cases of special qualifiers. So, here we ensure unknown values are
+			// not OR-ed by default.
+			name: "special qualifiers without special values",
+			query: Query{
+				Keywords: []string{"keyword"},
+				Qualifiers: Qualifiers{
+					Is: []string{"foo", "bar"},
+					In: []string{"foo", "bar"},
+				},
+			},
+			out: `keyword in:bar in:foo is:bar is:foo`,
+		},
+		{
+			name: "non-special qualifiers used multiple times",
+			query: Query{
+				Keywords: []string{"keyword"},
+				Qualifiers: Qualifiers{
+					In:      []string{"foo", "bar"}, // "in:" is a special qualifier but its values here are not special
+					Is:      []string{"foo", "bar"}, // "is:" is a special qualifier but its values here are not special
+					Label:   []string{"foo", "bar"},
+					License: []string{"foo", "bar"},
+					No:      []string{"foo", "bar"},
+					Topic:   []string{"foo", "bar"},
+				},
+			},
+			out: `keyword in:bar in:foo is:bar is:foo label:bar label:foo license:bar license:foo no:bar no:foo topic:bar topic:foo`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.out, tt.query.AdvancedIssueSearchString())
 		})
 	}
 }
diff --git a/pkg/search/searcher_test.go b/pkg/search/searcher_test.go
index fb7bb616a..f5467be37 100644
--- a/pkg/search/searcher_test.go
+++ b/pkg/search/searcher_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/MakeNowJust/heredoc"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/stretchr/testify/assert"
 )
@@ -265,7 +266,7 @@ func TestSearcherCode(t *testing.T) {
 			if tt.host == "" {
 				tt.host = "github.com"
 			}
-			searcher := NewSearcher(client, tt.host)
+			searcher := NewSearcher(client, tt.host, &fd.DisabledDetectorMock{})
 			result, err := searcher.Code(tt.query)
 			if tt.wantErr {
 				assert.EqualError(t, err, tt.errMsg)
@@ -551,7 +552,7 @@ func TestSearcherCommits(t *testing.T) {
 			if tt.host == "" {
 				tt.host = "github.com"
 			}
-			searcher := NewSearcher(client, tt.host)
+			searcher := NewSearcher(client, tt.host, &fd.DisabledDetectorMock{})
 			result, err := searcher.Commits(tt.query)
 			if tt.wantErr {
 				assert.EqualError(t, err, tt.errMsg)
@@ -837,7 +838,7 @@ func TestSearcherRepositories(t *testing.T) {
 			if tt.host == "" {
 				tt.host = "github.com"
 			}
-			searcher := NewSearcher(client, tt.host)
+			searcher := NewSearcher(client, tt.host, &fd.DisabledDetectorMock{})
 			result, err := searcher.Repositories(tt.query)
 			if tt.wantErr {
 				assert.EqualError(t, err, tt.errMsg)
@@ -1123,7 +1124,7 @@ func TestSearcherIssues(t *testing.T) {
 			if tt.host == "" {
 				tt.host = "github.com"
 			}
-			searcher := NewSearcher(client, tt.host)
+			searcher := NewSearcher(client, tt.host, fd.AdvancedIssueSearchUnsupported())
 			result, err := searcher.Issues(tt.query)
 			if tt.wantErr {
 				assert.EqualError(t, err, tt.errMsg)
@@ -1135,6 +1136,88 @@ func TestSearcherIssues(t *testing.T) {
 	}
 }
 
+func TestSearcherIssuesAdvancedSyntax(t *testing.T) {
+	query := Query{
+		Kind:     KindIssues,
+		Limit:    1,
+		Keywords: []string{"keyword"},
+		Qualifiers: Qualifiers{
+			// Ordinary qualifiers
+			Author: "johndoe",
+			Label:  []string{"foo", "bar"},
+			// Special qualifiers (that should be grouped and OR-ed when using advanced issue search)
+			Repo: []string{"foo/bar", "foo/baz"},
+			Is:   []string{"private", "public"},
+			User: []string{"johndoe", "janedoe"},
+			In:   []string{"title", "body", "comments"},
+		},
+	}
+
+	tests := []struct {
+		name       string
+		query      Query
+		detector   fd.Detector
+		wantValues url.Values
+		wantErr    string
+	}{
+		{
+			// TODO advancedIssueSearchCleanup
+			// Remove this test case once GHES 3.17 support ends.
+			name:     "advanced issue search not supported",
+			detector: fd.AdvancedIssueSearchUnsupported(),
+			query:    query,
+			wantValues: url.Values{
+				"q":               []string{"keyword author:johndoe in:body in:comments in:title is:private is:public label:bar label:foo repo:foo/bar repo:foo/baz user:janedoe user:johndoe"},
+				"advanced_search": nil, // assert absence
+			},
+		},
+		{
+			// TODO advancedIssueSearchCleanup
+			// Remove this test case once GHES 3.17 support ends.
+			name:     "advanced issue search supported as an opt-in feature",
+			detector: fd.AdvancedIssueSearchSupportedAsOptIn(),
+			query:    query,
+			wantValues: url.Values{
+				"q":               []string{"keyword author:johndoe (in:body OR in:comments OR in:title) (is:private OR is:public) label:bar label:foo (repo:foo/bar OR repo:foo/baz) (user:janedoe OR user:johndoe)"},
+				"advanced_search": []string{"true"}, // opt-in
+			},
+		},
+		{
+			// TODO advancedIssueSearchCleanup
+			// No need for feature detection once GHES 3.17 support ends.
+			name:     "advanced issue search supported as the only search backend",
+			detector: fd.AdvancedIssueSearchSupportedAsOnlyBackend(),
+			query:    query,
+			wantValues: url.Values{
+				"q":               []string{"keyword author:johndoe (in:body OR in:comments OR in:title) (is:private OR is:public) label:bar label:foo (repo:foo/bar OR repo:foo/baz) (user:janedoe OR user:johndoe)"},
+				"advanced_search": nil, // assert absence
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			reg := &httpmock.Registry{}
+			defer reg.Verify(t)
+
+			reg.Register(
+				httpmock.QueryMatcher("GET", "search/issues", tt.wantValues),
+				httpmock.JSONResponse(IssuesResult{}),
+			)
+
+			client := &http.Client{Transport: reg}
+			searcher := NewSearcher(client, "github.com", tt.detector)
+
+			_, err := searcher.Issues(tt.query)
+			if tt.wantErr != "" {
+				assert.EqualError(t, err, tt.wantErr)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
 func TestSearcherURL(t *testing.T) {
 	query := Query{
 		Keywords: []string{"keyword"},
@@ -1179,7 +1262,7 @@ func TestSearcherURL(t *testing.T) {
 			if tt.host == "" {
 				tt.host = "github.com"
 			}
-			searcher := NewSearcher(nil, tt.host)
+			searcher := NewSearcher(nil, tt.host, nil)
 			assert.Equal(t, tt.url, searcher.URL(tt.query))
 		})
 	}