# Turn Linear Issues Into Pull Requests for a Symfony App

Yes. Give the run a Linear issue and your Symfony repository: the agent changes the blog controller, runs PHPUnit and PHPStan and opens a linked pull request.

[Run this on Rush](https://cloud.getrush.ai/run/linear-issue-to-pr/symfony/), in a Project that has your repository.

## What the agent does

The issue comes in as a resource. `resources` names the repository and the Linear issue URL, which Rush reads through your Linear connection and places in the prompt, so the description, the comments and the acceptance criteria are in front of the agent before it opens a file.

In a Symfony application like symfony/demo, `composer.json` asks for PHP 8.4 and pins Symfony to `8.1.*` in `extra.symfony.require`, with `phpunit/phpunit` 11.5, `phpstan/phpstan` 2 with the Doctrine and Symfony extensions, `friendsofphp/php-cs-fixer` 3.92, `dama/doctrine-test-bundle` and `doctrine/doctrine-fixtures-bundle` in `require-dev`. `phpunit.dist.xml` sets `failOnDeprecation`, `failOnNotice` and `failOnWarning` to true and loads the DAMA extension, so every test runs in a transaction that is rolled back. `.env.test` points `DATABASE_URL` at `data/database_test.sqlite`, and that file is committed, as is `data/database.sqlite` for `dev`.

The public blog is `src/Controller/BlogController.php` under `#[Route('/blog')]`: `index` serves `blog_index`, the paginated `/page/{page}` and the `/rss.xml` feed, `postShow` serves `/posts/{slug:post}`, `commentNew` takes `POST /comment/{postSlug}/new`, and `search` answers `/search`. Admin is `src/Controller/Admin/BlogController.php` under `#[Route('/admin/post')]` with `#[IsGranted(User::ROLE_ADMIN)]` on the class, and `edit` and `delete` guarded again with `#[IsGranted('edit', subject: 'post')]` and `'delete'`, which a voter decides. Queries sit in `src/Repository/PostRepository.php`, `findLatest` returning a `Paginator` and `findBySearchQuery`, and `src/Entity/Post.php` carries the constraints: a non-blank title, a summary of at most 255 characters, content of at least 10, at most four tags.

A ticket about listing or search lands in `BlogController` and `PostRepository`; a ticket about who may edit lands in the voter. Tests go in `tests/Controller/BlogControllerTest.php`, where `testIndex` counts `Paginator::PAGE_SIZE` articles on `/en/blog/`, `testRss` checks the feed, `testNewComment` posts a comment and `testAjaxSearch` queries the search route, or in `tests/Controller/Admin/BlogControllerTest.php`, which logs a user in with `loginUser` and covers new, edit and delete. The agent runs `composer install`, `vendor/bin/phpunit`, `vendor/bin/phpstan analyze` at the configured level 6 and the console lints from `lint.yaml`, `lint:twig`, `lint:yaml`, `lint:container` and `doctrine:schema:validate --skip-sync`, and opens the pull request.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "codex",
  "input": "Implement the Linear issue given as a resource in this Symfony codebase. Read the issue and its comments first. Make the change in the blog controller, admin controller, repository, entity or voter the issue concerns, and keep the diff small. Add PHPUnit tests in tests/Controller next to the existing ones. Run `composer install`, `vendor/bin/phpunit`, `vendor/bin/phpstan analyze --no-progress`, `bin/console lint:twig templates`, `bin/console lint:container` and `bin/console doctrine:schema:validate --skip-sync`, and fix what fails. Open a pull request whose description links the issue, explains the change and quotes the test result. Do not merge.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    },
    {
      "url": "https://linear.app/your-org/issue/ENG-42"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

A pull request on its own branch comes back. Its description links the issue, names the controller method, repository query or voter it changed, and quotes the PHPUnit summary with the PHPStan and lint results beside it. The diff holds the change and its test.

The pull request URL is on the session and in the `run.completed` webhook, and your handler posts it as a comment on the issue, because the sandbox has no Linear tools. The transcript keeps the install, each command and every file the agent read, so a reviewer can see that a change under `#[IsGranted]` was made after reading the voter and `Admin/BlogControllerTest.php`.

Before merging, a reviewer checks the translations: routes carry a locale, `testIndex` hits `/en/blog/`, and a new string needs an entry under `translations/` or the `lint:translations` step in CI flags it.

## Run it on every Linear issue labeled agent

Create an automation with a `linear` trigger on `linear.issue.labeled`, the hand-off label in `config.labels` and the team keys in `config.teams`. Each labeled issue starts one run with the issue as its context and this prompt; a label added by an app integration never fires it.

A `linear.issue.state_changed` trigger with `config.states` mapping a review state to `plan` and a later one to `exec` puts a read-only run first: it posts a plan as a comment on the issue and changes nothing, and the `exec` run opens the pull request once someone has read it.

## Why run it on Rush?

The demo is small, but its CI is strict: PHPUnit fails on any deprecation, notice or warning, PHPStan runs at level 6 over `bin`, `config`, `public`, `src` and `tests`, and `composer audit` runs with the lints. On Rush all of that runs in a sandbox, in parallel with other tickets, and the output stays with the run instead of in a terminal that was closed.

The pull request quotes PHPUnit and PHPStan, and the transcript shows which controller and voter the agent read before touching an `#[IsGranted]` attribute, which is what a reviewer wants to see on an access change. Nobody's local SQLite file is touched: the sandbox has its own copy of `data/database_test.sqlite`.

## Frequently asked questions

### How does the agent read the issue?

From a `resources` entry with the Linear issue URL, read through your Linear connection and placed in the prompt before the run starts. Connect Linear once with `POST /me/connections {"type": "app", "name": "linear"}` and finish the sign-in it returns.

### Can it comment on the issue?

Not from the sandbox: a Linear sign-in cannot yet be named in `connections`. The pull request URL comes back on the session and in the webhook, and your handler posts it.

### Does it need a database server?

No. `.env.test` uses SQLite at `data/database_test.sqlite`, committed with the repository, and the DAMA bundle rolls each test's transaction back, so the file is unchanged after the suite.

### Will it respect the deprecation policy?

The suite fails on deprecations, notices and warnings by configuration, so a change that triggers one fails the run and the agent fixes it before opening the pull request, or says in the description why it could not.
