Skip to content

bin/altair doctor — health checks with agent-actionable output #68

@tonydspaniard

Description

@tonydspaniard

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

  • bin/altair doctor works on an empty Altair-skeleton project (returns ok for all applicable checks)
  • --format=json produces parseable JSON conforming to the shape above
  • Each shipped check has accurate detail text and at least one of (fix, agent_action) when status ≠ ok
  • Dependency-aware execution: failures in upstream checks skip downstream ones with status skipped
  • --fix attempts safe auto-fixes for checks that opt in (e.g. manifest:generate, cs:fix); no destructive operations
  • Exit code reflects worst observed status (0/1/2)
  • User-defined checks register cleanly via the container
  • Tests:
    • Each built-in check: golden-state passes, induced-failure-state surfaces the right agent_action
    • JsonRenderer produces deterministic output (no timestamps in detail, ordered keys)
    • DoctorCommand integration: build a fake project tree with known issues, assert JSON output
    • Skipped-due-to-upstream logic tested

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions