What the agent does
The agent reads how routing and security are laid out before it adds a route. In a Symfony app shaped like symfony/demo, config/routes.yaml imports the attribute routes from src/Controller through the routing.controllers resource under a /{_locale} prefix, so BlogController's /blog becomes /en/blog/, and config/packages/security.yaml puts one lazy main firewall over every URL with a dev firewall that switches security off for _profiler, _wdt, assets and build. A health endpoint must live outside the locale prefix, because a load balancer will not send Accept-Language, and must answer without a session or a login. The agent adds a HealthController whose routes are declared with #[Route('/health/live')] and #[Route('/health/ready')] and imported in routes.yaml as a separate resource without the prefix. RedirectToPreferredLocaleSubscriber in src/EventSubscriber only acts on /, and security.yaml has no access_control list, so nothing in the request pipeline redirects a probe at those paths.
The live route returns 200 with a small JSON body and touches nothing. The ready route runs SELECT 1 through the Doctrine DBAL Connection that config/packages/doctrine.yaml configures from DATABASE_URL, which in .env points at data/database.sqlite and in .env.test at data/database_test.sqlite, with a short timeout, and answers 200 or 503 with a checks object naming the database. The controller is a plain service, autowired through the App\ resource in config/services.yaml, so it needs no service definition; PHP-CS-Fixer rules in .php-cs-fixer.dist.php and the level 6 PHPStan configuration in phpstan.dist.neon, with the Symfony and Doctrine extensions, both apply to it.
Tests follow the pattern already in tests/Controller. DefaultControllerTest extends WebTestCase, lists public URLs in a data provider and asserts assertResponseIsSuccessful, and secure URLs redirect to /en/login; BlogControllerTest uses loginUser and relies on the DAMA Doctrine test bundle, enabled in config/packages/dama_doctrine_test_bundle.yaml, to roll back database writes. The agent writes a HealthControllerTest that requests both routes without a locale and without logging in, asserts the JSON body of each, and covers the 503 branch by replacing the connection in the test container with one whose executeQuery throws. phpunit.dist.xml sets failOnDeprecation, failOnNotice and failOnWarning, so a deprecated call in the new controller fails the suite rather than printing a notice.
Then it runs what the two workflows run: composer install, vendor/bin/phpunit as tests.yaml does on PHP 8.4 and 8.5, and the lint.yaml steps that matter for a new route, bin/console lint:container --no-debug, bin/console lint:yaml config --parse-tags and bin/console doctrine:schema:validate --skip-sync, plus vendor/bin/php-cs-fixer check --diff and vendor/bin/phpstan. The pull request holds the controller, the routing import, the test, and a line in README.md naming the two paths.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Add health checks to this Symfony application. Read config/routes.yaml, config/packages/security.yaml, config/packages/doctrine.yaml, config/services.yaml, src/EventSubscriber/RedirectToPreferredLocaleSubscriber.php and tests/Controller/DefaultControllerTest.php first. Add GET /health/live, which answers 200 with a JSON body and no dependencies, and GET /health/ready, which runs SELECT 1 through the Doctrine DBAL connection with a timeout and answers 200 or 503 with a checks object. Register both outside the /{_locale} prefix so they never redirect, and make sure they are reachable without a session or login. Write PHPUnit tests on WebTestCase for both routes and both outcomes of the ready check, in the style of the existing controller tests. Run composer install, vendor/bin/phpunit, vendor/bin/php-cs-fixer check --diff, vendor/bin/phpstan, bin/console lint:container --no-debug and bin/console lint:yaml config --parse-tags, and fix what fails. Add one line to README.md naming the two paths. Open a pull request that lists the files and quotes the phpunit summary. Do not change any other route.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- 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.
- 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.
- 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
What comes back is a pull request on a new branch with four files: src/Controller/HealthController.php, the import added to config/routes.yaml, tests/Controller/HealthControllerTest.php and a line in README.md. The description names the two paths, says why they sit outside the locale prefix, and quotes the phpunit summary and the php-cs-fixer and phpstan results from the sandbox.
The session read names the repository and the pull request URL, and the transcript keeps each command with its output, including the first phpunit run if a test failed before the fix. A reviewer checks that /health/ready really is exempt from the locale redirect and the login redirect, that the SELECT 1 runs with a timeout rather than hanging on a stalled database, and that no response leaks the DSN from DATABASE_URL.
Nothing else moves: composer.lock, importmap.php and the existing controllers are unchanged, and the workflows are not edited.
Run it on every Linear issue labelled agent
Create an automation in the Project with this prompt and a linear trigger on linear.issue.labeled, with the label your team uses to hand work off in config.labels and, if only some teams should fire it, their keys in config.teams. Labelling the issue that asks for health checks starts one run with that issue as context; the trigger adds the issue after the prompt and replaces nothing in it. Changes made by an app integration never fire the trigger.
From your own server, the same run starts with POST /o/{org}/sessions and this body, plus the issue URL as a second resources entry (https://linear.app/<workspace>/issue/<ID>), which Rush reads through your Linear connection and places in the prompt. The pull request URL comes back on the session and in the run.completed webhook when the key has a webhook_url.
The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.
Why run it on Rush?
A health route is small, but proving it is right needs PHP 8.4 with intl and pdo_sqlite, a composer install, the test database at data/database_test.sqlite, and five tools run in the right order. The sandbox has all of that for the length of the run and parks with its disk when the turn ends, so a follow-up such as adding a cache check to /health/ready picks up where the first turn left off.
The pull request carries the proof: the test file, the quoted phpunit summary and a transcript showing the lint commands and their output. A reviewer judging whether a probe can ever redirect to /en/login sees the test that requests it without a session, not a sentence saying it was checked.
Frequently asked questions
Why two routes and not one /health?
A liveness probe should never fail because the database is slow, or an orchestrator would restart a healthy process. /health/live answers from memory; /health/ready is the one a load balancer uses to decide whether to send traffic, and it is the one that runs SELECT 1.
Will the routes be behind the firewall or the locale redirect?
Neither. They are imported in config/routes.yaml without the /{_locale} prefix, and the main firewall is lazy, so an anonymous request that never reads the session is served without a redirect. The test requests both routes with no login and asserts a 200.
Does the ready check open a new database connection?
No. It uses the DBAL Connection Doctrine already configures from DATABASE_URL, with a per-call timeout, so the probe sees the same database the application sees, SQLite in the demo and PostgreSQL or MySQL if you change the URL.
What about the HTTP cache and ESI the app turns on?
config/packages/framework.yaml enables esi and fragments, and BlogController::index sets #[Cache(smaxage: 10)]. The health responses set no cache attribute and send Cache-Control: no-store, so a proxy never serves a stale 200 for a database that has gone away.