Goal
Build bin/altair doctor — a single command that runs a battery of health checks against an Altair-based project and emits structured, machine-readable output an AI agent can parse and act on without scraping.
When something is wrong, the agent reads the check result and knows the exact next action to take. This collapses 80% of "the agent got stuck" failure modes.
Why
Vibe coding is a feedback loop: agent writes code → tests run → something breaks → agent reads the error → fixes → repeat. The bottleneck is the "agent reads the error" step. Standard error messages (PHP traces, "column not found", "service not bound") are human-shaped — the agent has to interpret them. doctor replaces interpretation with explicit machine-readable next actions.
Think of it as a permanent debugging companion that's always one command away.
Checks shipped in v1
The check registry is extensible; v1 ships these:
| Check |
What it verifies |
Common failure |
php_version |
Active PHP ≥ project's composer.json floor |
Wrong PHP on PATH |
extensions_loaded |
Required ext-* are loaded |
Missing redis / memcached / etc. |
composer_deps |
vendor/ is current with composer.lock |
Stale dependencies |
container_boots |
The application Container can be constructed without errors |
Missing Configuration binding |
container_resolves_critical_bindings |
Key contracts resolve (MiddlewareInterface, EntityManagerInterface, etc.) |
Missing or misconfigured Configuration |
database_reachable |
If persistence is configured, can the DB be connected to |
Wrong DB URL, container not running |
migrations_pending |
No unapplied migrations |
Forgot to run db:migrate |
spec_drift |
All scaffolded files still match their YAML spec |
Hand-edited generated code |
openapi_valid |
The emitted OpenAPI document validates against 3.1 schema |
Hand-edited spec broke format |
manifests_current |
.agent/packages/*.md matches what the generator would produce now |
Forgot to run manifest:generate after a change |
tests_passing |
vendor/bin/phpunit exits 0 |
Test failure |
phpstan_clean |
vendor/bin/phpstan exits 0 (with baseline) |
New type error |
cs_clean |
vendor/bin/php-cs-fixer fix --dry-run exits 0 |
Lint drift |
doctor runs them in dependency order; later checks skip when prerequisites failed (no point running tests_passing if composer_deps is broken).
CLI surface
bin/altair doctor # human-friendly default
bin/altair doctor --format=json # machine-readable
bin/altair doctor --format=json --quiet # JSON only, no other stdout/stderr
bin/altair doctor --only=spec_drift,tests_passing # specific checks only
bin/altair doctor --skip=phpstan_clean # exclude checks
bin/altair doctor --fix # attempt safe auto-fixes where check provides one
Exit code: 0 if all checks pass, 1 if any check is warn, 2 if any is error.
Output shape (--format=json)
{
"status": "warn",
"duration_ms": 1284,
"checks": [
{
"name": "php_version",
"status": "ok",
"detail": "PHP 8.3.31 satisfies >=8.3"
},
{
"name": "spec_drift",
"status": "warn",
"detail": "CreateUserInput has no $lastName field but spec api/users/create.yaml declares it",
"fix": "Edit src/App/Http/Inputs/CreateUserInput.php to add `public readonly string $lastName`",
"agent_action": {
"type": "edit_file",
"file": "src/App/Http/Inputs/CreateUserInput.php",
"hint": "add_property lastName string"
},
"source": "api/users/create.yaml"
},
{
"name": "manifests_current",
"status": "warn",
"detail": ".agent/packages/http.md is 3 revisions behind src/Altair/Http/",
"fix": "Run `bin/altair manifest:generate`",
"agent_action": {
"type": "run_command",
"command": "bin/altair manifest:generate"
}
}
]
}
The agent_action block is the differentiator — it tells an LLM exactly what tool call to make (edit a file, run a command, install a dep), removing the "now what?" guess work.
Human output
Default text mode is pretty but scannable — green ticks for ok, yellow warns with the fix line directly under, red errors with the agent_action quoted as a shell snippet. No frames or boxes; agents tail this in case the user pipes it.
Shape
src/Altair/Doctor/
├── Cli/
│ └── DoctorCommand.php
├── Contracts/
│ └── CheckInterface.php # name, status, detail, fix, agent_action
├── Check/ # one file per built-in check
│ ├── PhpVersionCheck.php
│ ├── ExtensionsLoadedCheck.php
│ ├── ComposerDepsCheck.php
│ ├── ContainerBootsCheck.php
│ ├── ContainerResolvesCheck.php
│ ├── DatabaseReachableCheck.php
│ ├── MigrationsPendingCheck.php
│ ├── SpecDriftCheck.php
│ ├── OpenApiValidCheck.php
│ ├── ManifestsCurrentCheck.php
│ ├── TestsPassingCheck.php
│ ├── PhpstanCleanCheck.php
│ └── CsCleanCheck.php
├── Output/
│ ├── JsonRenderer.php
│ └── HumanRenderer.php
├── CheckRegistry.php
├── Configuration/
│ └── DoctorConfiguration.php # adds custom checks
└── composer.json
Extensibility
User-defined checks register via the container:
$container->define(MyCustomCheck::class, ...);
$container->prepare(CheckRegistry::class, function (CheckRegistry $r, Container $c): void {
$r->add($c->make(MyCustomCheck::class));
});
Or via attribute on a class implementing CheckInterface:
#[DoctorCheck(name: 'my_custom', description: '...')]
final class MyCustomCheck implements CheckInterface { ... }
Acceptance criteria
Out of scope
- A persistent "watch" mode (doctor is invoked on demand; rerun manually)
- Suggesting library updates (a separate
bin/altair upgrade would own that)
- Performance benchmarking (separate concern, doesn't belong here)
- Auto-fixes for anything destructive (data deletes, downgrades, force-pushes)
Dependencies
A minimal v0 of doctor (only the checks whose prerequisites are met) can ship right after #17, with later checks added as their dependencies land. So this issue is sequenced parallel with the build-out, not strictly after all of them.
Goal
Build
bin/altair doctor— a single command that runs a battery of health checks against an Altair-based project and emits structured, machine-readable output an AI agent can parse and act on without scraping.When something is wrong, the agent reads the check result and knows the exact next action to take. This collapses 80% of "the agent got stuck" failure modes.
Why
Vibe coding is a feedback loop: agent writes code → tests run → something breaks → agent reads the error → fixes → repeat. The bottleneck is the "agent reads the error" step. Standard error messages (PHP traces, "column not found", "service not bound") are human-shaped — the agent has to interpret them.
doctorreplaces interpretation with explicit machine-readable next actions.Think of it as a permanent debugging companion that's always one command away.
Checks shipped in v1
The check registry is extensible; v1 ships these:
php_versioncomposer.jsonfloorextensions_loadedext-*are loadedcomposer_depsvendor/is current withcomposer.lockcontainer_bootscontainer_resolves_critical_bindingsMiddlewareInterface,EntityManagerInterface, etc.)database_reachablemigrations_pendingdb:migratespec_driftopenapi_validmanifests_current.agent/packages/*.mdmatches what the generator would produce nowmanifest:generateafter a changetests_passingvendor/bin/phpunitexits 0phpstan_cleanvendor/bin/phpstanexits 0 (with baseline)cs_cleanvendor/bin/php-cs-fixer fix --dry-runexits 0doctorruns them in dependency order; later checks skip when prerequisites failed (no point runningtests_passingifcomposer_depsis broken).CLI surface
Exit code:
0if all checks pass,1if any check iswarn,2if any iserror.Output shape (
--format=json){ "status": "warn", "duration_ms": 1284, "checks": [ { "name": "php_version", "status": "ok", "detail": "PHP 8.3.31 satisfies >=8.3" }, { "name": "spec_drift", "status": "warn", "detail": "CreateUserInput has no $lastName field but spec api/users/create.yaml declares it", "fix": "Edit src/App/Http/Inputs/CreateUserInput.php to add `public readonly string $lastName`", "agent_action": { "type": "edit_file", "file": "src/App/Http/Inputs/CreateUserInput.php", "hint": "add_property lastName string" }, "source": "api/users/create.yaml" }, { "name": "manifests_current", "status": "warn", "detail": ".agent/packages/http.md is 3 revisions behind src/Altair/Http/", "fix": "Run `bin/altair manifest:generate`", "agent_action": { "type": "run_command", "command": "bin/altair manifest:generate" } } ] }The
agent_actionblock is the differentiator — it tells an LLM exactly what tool call to make (edit a file, run a command, install a dep), removing the "now what?" guess work.Human output
Default text mode is pretty but scannable — green ticks for ok, yellow warns with the fix line directly under, red errors with the agent_action quoted as a shell snippet. No frames or boxes; agents tail this in case the user pipes it.
Shape
Extensibility
User-defined checks register via the container:
Or via attribute on a class implementing
CheckInterface:Acceptance criteria
bin/altair doctorworks on an empty Altair-skeleton project (returns ok for all applicable checks)--format=jsonproduces parseable JSON conforming to the shape abovedetailtext and at least one of (fix,agent_action) when status ≠ okskipped--fixattempts safe auto-fixes for checks that opt in (e.g.manifest:generate,cs:fix); no destructive operationsagent_actionJsonRendererproduces deterministic output (no timestamps indetail, ordered keys)DoctorCommandintegration: build a fake project tree with known issues, assert JSON outputOut of scope
bin/altair upgradewould own that)Dependencies
univeros/cli) — required (it's a CLI command)univeros/agent-spec) — required formanifests_currentcheckuniveros/scaffold) — required forspec_driftandopenapi_validchecksuniveros/persistence) — required fordatabase_reachable,migrations_pendingA minimal v0 of
doctor(only the checks whose prerequisites are met) can ship right after #17, with later checks added as their dependencies land. So this issue is sequenced parallel with the build-out, not strictly after all of them.