Skip to content

Prep structured agent output and implement agentFix() on free rules#35

Merged
PovilasKorop merged 2 commits into
mainfrom
agent-flag
Apr 13, 2026
Merged

Prep structured agent output and implement agentFix() on free rules#35
PovilasKorop merged 2 commits into
mainfrom
agent-flag

Conversation

@krekas
Copy link
Copy Markdown
Collaborator

@krekas krekas commented Apr 13, 2026

This PR lays the groundwork for the upcoming FilaCheck-Pro structured-output mode (where an AI coding agent runs filacheck and consumes a single JSON document on stdout) and ships the free-package half of the contract end-to-end:

  1. Scaffolding — a small set of new abstractions in the free package that Pro can plug into without the free package having any dependency on agent-detection logic.
  2. Rule data — every rule under src/Rules/ now returns a structured, agent-actionable fix payload (instructions, next_steps, docs) so agents get a concrete plan instead of having to parse the human suggestion string.

The default vendor/bin/filacheck run against a terminal is byte-for-byte unchanged — the new code paths are only exercised when Pro flips them on.

The work is split across two commits.


Commit 1 — Prep structured agent output for pro version

Adds the abstractions Pro needs to inject a structured reporter at runtime, plus a command-execution path in CodeFixer so Pro rules can ship "run php artisan make:filament-theme admin"-style fixes the same way the existing text-replacement rules do.

New files

File Purpose
src/Rules/ProvidesAgentFix.php Optional rule extension. Rules that implement it return JSON-serializable fix data per violation.
src/Reporting/ReporterInterface.php Common contract (report / reportWithFixes) so reporters are pluggable.
src/Reporting/ReporterFactory.php Static registry that lets Pro override(...) the default reporter at runtime — e.g. swap in JsonReporter when an agent is detected.
src/Support/RunContext.php Process-wide flag (markAgent / isAgent) Pro sets when it detects an AI agent. The free CLI reads this flag — no Pro dependency.

Modified files

  • src/Rules/FixableRule.php — docblock now documents two flavours of fixable violation: the existing text replacement (byte-precise edit of the offending file) and the new command execution (run an external command from the project root).
  • src/Support/Violation.php — adds two nullable fields:
    public ?string $fixCommand = null,
    public ?string $fixCommandCwd = null,
  • src/Fixer/CodeFixer.php — partitions violations into text-style and command-style at the start of fix(), and gains a fixCommands() path that:
    • dedupes by (fixCommand, fixCommandCwd) so the same php artisan ... only runs once per scan even when many Blade files trigger it,
    • refuses to execute when the resolved cwd lacks an artisan file (guard against a misconfigured rule passing the wrong base path),
    • supports --dry-run (marks status would-run),
    • uses symfony/process (now a hard dependency in composer.json),
    • returns a new commands key in the fix() result describing every command's status / output.
  • bin/filacheck — three behavioural changes, all gated on RunContext::isAgent():
    1. $reporter = StandaloneReporter::new(...) is replaced with ReporterFactory::make(...) so Pro can swap it out.
    2. When RunContext::isAgent() is true, $fix is forced to true so CodeFixer applies all auto-fixable issues with byte precision before any structured reporter emits output. This avoids handing the agent ambiguous search/replace strings for fixes the tool can already apply itself.
    3. Progress chatter ("Scanning: …") moves to stderr and is silenced entirely in agent mode so stdout stays clean for a single parseable JSON document.
  • src/Reporting/ConsoleReporter.php + src/Reporting/StandaloneReporter.php — now implements ReporterInterface. Behaviour unchanged.
  • composer.json — adds symfony/process: ^6.0|^7.0|^8.0 for the command-execution path.
  • resources/boost/guidelines/core.blade.php — new Boost guideline that tells Laravel Boost agents to run vendor/bin/filacheck --fix after touching anything under app/Filament.

Worked example: ProvidesAgentFix interface

namespace Filacheck\Rules;

use Filacheck\Support\Violation;

interface ProvidesAgentFix
{
    /**
     * @return mixed JSON-serializable fix data, or null
     */
    public function agentFix(Violation $violation): mixed;
}

Worked example: RunContext

final class RunContext
{
    private static bool $agentMode = false;

    public static function markAgent(): void { self::$agentMode = true; }
    public static function isAgent(): bool { return self::$agentMode; }
    public static function reset(): void { self::$agentMode = false; }
}

Worked example: agent-mode forcing in bin/filacheck

// In agent mode, force --fix so CodeFixer applies all auto-fixable issues
// with byte-precise positions before any structured reporter (e.g.
// JsonReporter) emits output. This avoids handing the agent ambiguous
// search/replace strings for fixes the tool can already apply itself.
if (RunContext::isAgent()) {
    $fix = true;
}

Commit 2 — Provide structured agent fix data on free rules

Implements ProvidesAgentFix on all 16 rules in src/Rules/ so the JSON output Pro emits for a free-rule violation gets a concrete fix payload instead of fix: null.

Pattern

Every rule now implements <existingInterface>, ProvidesAgentFix, gains the ResolvesFilamentDocsUrl trait if it didn't already have it, and adds an agentFix(Violation $violation): mixed method that returns:

[
    'instructions' => string,    // 1-sentence imperative directive
    'next_steps'   => string[],  // 2–5 concrete bullets, including edge cases
    'docs'         => string,    // filamentDocsUrl(...) link
]

This shape mirrors the convention already in use in FilaCheck-Pro/src/Rules, so Pro's JsonReporter consumes free-rule and pro-rule fix payloads through a single code path.

Worked example: DeprecatedReactiveRule

-class DeprecatedReactiveRule implements FixableRule
+class DeprecatedReactiveRule implements FixableRule, ProvidesAgentFix
 {
     use CalculatesLineNumbers;
     use ResolvesFilamentDocsUrl;

     // …existing check() unchanged…

+    public function agentFix(Violation $violation): mixed
+    {
+        return [
+            'instructions' => 'Replace the deprecated `reactive()` modifier with `live()`.',
+            'next_steps' => [
+                'Rename `->reactive()` to `->live()` on the field.',
+                'If you need debounced updates, use `->live(debounce: 500)`. If you only want to react on blur, use `->live(onBlur: true)`.',
+            ],
+            'docs' => $this->filamentDocsUrl('forms/overview#the-basics-of-reactivity'),
+        ];
+    }
 }

Rules updated

# Rule Theme
1 ActionInBulkActionGroupRule Action::make() inside toolbarActions()BulkAction::make()
2 DeprecatedActionFormRule action ->form([...])->schema([...])
3 DeprecatedBulkActionsRule table ->bulkActions([...])->toolbarActions([...])
4 DeprecatedEmptyLabelRule ->label('')->hiddenLabel() / ->iconButton()
5 DeprecatedFilterFormRule filter ->form([...])->schema([...])
6 DeprecatedFormsGetRule Filament\Forms\GetFilament\Schemas\Components\Utilities\Get
7 DeprecatedFormsSetRule Filament\Forms\SetFilament\Schemas\Components\Utilities\Set
8 DeprecatedGetTableQueryRule getTableQuery()->query() inside table()
9 DeprecatedImageColumnSizeRule ImageColumn::size()->imageSize()
10 DeprecatedMutateFormDataUsingRule mutateFormDataUsing()mutateDataUsing()
11 DeprecatedPlaceholderRule Placeholder::make()TextEntry::make()->state(...)
12 DeprecatedReactiveRule ->reactive()->live()
13 DeprecatedTestMethodsRule v3 testing helpers → v4 TestAction::make(...) wrappers
14 DeprecatedUrlParametersRule renamed Livewire URL query keys
15 DeprecatedViewPropertyRule $viewprotected string $view
16 WrongTabNamespaceRule Filament\Tabs\TabFilament\Schemas\Components\Tabs\Tab

New test: tests/Rules/ProvidesAgentFixContractTest.php

A single dataset-driven contract test covers all 16 rules:

it('implements ProvidesAgentFix and returns a JSON-serializable structured fix', function (string $ruleClass) {
    $rule = new $ruleClass;

    expect($rule)->toBeInstanceOf(ProvidesAgentFix::class);

    $violation = new Violation(
        level: 'warning',
        message: 'The `assertFormSet()` method is deprecated.',
        file: 'app/Filament/Resources/UserResource.php',
        line: 42,
        rule: $rule->name(),
    );

    $fix = $rule->agentFix($violation);

    expect($fix)
        ->toBeArray()
        ->toHaveKeys(['instructions', 'next_steps', 'docs']);

    expect($fix['instructions'])->toBeString()->not->toBeEmpty();
    expect($fix['next_steps'])->toBeArray()->not->toBeEmpty();

    foreach ($fix['next_steps'] as $step) {
        expect($step)->toBeString()->not->toBeEmpty();
    }

    expect($fix['docs'])->toBeString()->toStartWith('https://filamentphp.com/');

    expect(json_encode($fix))->toBeString()->not->toBeFalse();
})->with('agent-fix rules');

The test asserts the shape of the payload (and that it round-trips through json_encode), not the exact wording — so future copy tweaks don't churn the suite.


Backwards compatibility

  • Default CLI behaviour is unchanged. With no Pro plugin loaded, RunContext::isAgent() is always false, so bin/filacheck still resolves StandaloneReporter from ReporterFactory::make(...), still writes "Scanning: …" to the terminal, and still only runs --fix when the user passes the flag.
  • Rule contract is additive. ProvidesAgentFix is an optional interface; nothing in the free package's reporter chain calls agentFix(). Rules that don't implement it will continue to emit fix: null once Pro's JsonReporter lands, exactly as today.
  • Violation constructor picks up two new optional fields at the end of the parameter list, so existing positional callers are unaffected.
  • composer.json gains symfony/process. This is a new hard dependency but matches the existing symfony/console constraint, so Laravel apps already satisfy it transitively.

Test results

Tests:    256 passed (962 assertions)
Duration: 1.25s

That's the previous 240 tests plus 16 new contract tests (one per rule) — every existing test still passes, no fixture changes were needed.

Example agent payload (post-Pro)

Once the matching Pro PR lands and an AI agent runs vendor/bin/filacheck, a single violation now serialises to JSON like:

{
  "rule": "deprecated-reactive",
  "file": "app/Filament/Resources/UserResource.php",
  "line": 87,
  "message": "The `reactive()` method is deprecated.",
  "fix": {
    "instructions": "Replace the deprecated `reactive()` modifier with `live()`.",
    "next_steps": [
      "Rename `->reactive()` to `->live()` on the field.",
      "If you need debounced updates, use `->live(debounce: 500)`. If you only want to react on blur, use `->live(onBlur: true)`."
    ],
    "docs": "https://filamentphp.com/docs/4.x/forms/overview#the-basics-of-reactivity"
  }
}

instead of:

{ "rule": "deprecated-reactive", "fix": null }

@PovilasKorop PovilasKorop merged commit 3911bc4 into main Apr 13, 2026
10 checks passed
@PovilasKorop PovilasKorop deleted the agent-flag branch April 13, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants