# Command bus, idempotency and audit trail

> **Diagram:** A command request carries an idempotency key. If the key was seen before, the stored result is replayed and nothing runs again. Otherwise the actor must hold the ability, the params must pass the rules, and then the handler runs. Refusals, invalid params and results are all written to the audit trail.

Every change can go through one bus, so a dashboard, an Artisan command, a job and an AI agent all get the same
rules: the actor must hold the command's **ability**, every request carries an **idempotency key** (a retry
replays the stored result instead of running twice), and each outcome is written to an **audit trail**.

```php
use ByteMinus\AiBlog\Bus\Actor;
use ByteMinus\AiBlog\Bus\CommandBus;
use ByteMinus\AiBlog\Bus\CommandContext;
use ByteMinus\AiBlog\Bus\CommandHandler;
use ByteMinus\AiBlog\Bus\CommandOutcome;
use ByteMinus\AiBlog\Bus\CommandRequest;

final class RenamePost extends CommandHandler
{
    public function ability(): ?string { return 'posts.write'; }

    public function rules(): array { return ['title' => ['required', 'string', 'max:120']]; }

    public function handle(CommandContext $context): CommandOutcome
    {
        // ...call your service here...
        return CommandOutcome::completed(['title' => $context->params['title']], 'post', '42');
    }
}
```

```php
// config/ai-blog.php
'commands' => ['handlers' => ['post.rename' => RenamePost::class]],

// AppServiceProvider: nobody manages the blog until you say who may
Gate::define('manageAiBlog', fn ($user) => $user->is_admin);

// anywhere
$receipt = app(CommandBus::class)->dispatch(
    Actor::user($request->user()),
    new CommandRequest('post.rename', idempotencyKey: 'rename-42-v1', params: ['title' => 'New title']),
);

$receipt->result();    // what the caller may see
$receipt->replayed;    // true when this was a retry
```

Pass `dryRun: true` to preview a command without changing anything. Publish and run the migrations to create the
`commands` and `audit_events` tables.
