> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# How to customize repro & fix suggestions

> Accept, reject, or rewrite Aspect CLI repro and fix command suggestions emitted by built-in Bazel tasks using TaskLifecycleTrait in config.axl.

Built-in tasks (`build`, `test`, `lint`, `format`, `gazelle`, `delivery`) emit two kinds of follow-up suggestions when something noteworthy happens:

* **Repro commands** — *"how do I reproduce this failure?"* Rendered with a `🔁 Reproduce:` header.
* **Fix commands** — *"how do I apply the suggested fix?"* Rendered with a `🛠️ Fix:` header.

The same suggestions appear everywhere a task's outcome is rendered: the CLI terminal output, Buildkite annotations, GitHub Status Check bodies, and the aggregate PR task summary comment.

`TaskLifecycleTrait.repro_fix_suggestion` lets your `.aspect/config.axl` accept, reject, or rewrite each suggestion before any surface renders it. Common uses:

* Rewrite suggested `aspect …` commands to your team's preferred form — for example `bazel …` via the [`tools/bazel` wrapper](/docs/cli/install#keep-your-team-typing-bazel-with-the-tools%2Fbazel-wrapper), or your repo's own custom developer CLI (e.g. `mycorp build …` instead of `aspect build …`) when you've wrapped Bazel/Aspect behind a company-branded entry point.
* Drop the vanilla-`bazel` alternate suggestion that built-ins tack on by default — useful when your team always has the Aspect CLI on PATH.
* Add a description to a suggestion that ships without one, so the rendered line carries a `# <label>` above the command explaining what it does.

## Hook signature

```python title=".aspect/config.axl" theme={null}
load(
    "@aspect//traits.axl",
    "REPRO_FIX_ACCEPT",
    "REPRO_FIX_REJECT",
    "ReproFixInfo",
    "ReproFixSuggestion",
    "TaskLifecycleTrait",
    "repro_fix_replace",
)

def _my_hook(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    # ...inspect info, return a verdict...
    return REPRO_FIX_ACCEPT

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_my_hook)
```

Hooks are invoked **in registration order** on every repro/fix entry, on every surface emit. Each entry runs through the chain at most once (idempotence is tracked internally), so it's safe to register hooks that intermix with other features that also touch repro/fix data.

## Verdicts

Return one of three values from your hook:

| Verdict                                     | Effect                                                                                                                       |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `REPRO_FIX_ACCEPT`                          | Keep the suggestion unchanged. Chain continues to the next hook.                                                             |
| `REPRO_FIX_REJECT`                          | Drop the suggestion entirely. Short-circuits the chain — no further hook sees it.                                            |
| `repro_fix_replace(command=, description=)` | Rewrite the command and/or description. Omit either field to keep the prior value. Chain continues with the rewritten entry. |

A `replace` with both `command` and `description` left as `None` is a no-op (equivalent to `accept`).

## `ReproFixInfo` fields

The single `info` argument carries everything a hook typically needs to decide:

| Field              | Type                  | Notes                                                                                                                                                                                                                                              |
| ------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `task_path`        | `str`                 | Joined `<group...> <kind>` CLI path, e.g. `"lint"`, `"dev test-repro-commands"`.                                                                                                                                                                   |
| `task_kind`        | `str`                 | The task kind — the command being run (e.g. `"lint"`, `"buildifier"`).                                                                                                                                                                             |
| `task_group`       | `list[str]`           | The group prefix (e.g. `[]`, `["dev"]`).                                                                                                                                                                                                           |
| `kind`             | `str`                 | Task-result kind — `"lint_results"`, `"bazel_results"`, `"format_results"`, `"gazelle_results"`, `"delivery_results"`. Matches `TaskUpdate.kind` so hooks can route by the same key the surface templates use.                                     |
| `command_kind`     | `ReproFixCommandKind` | `"repro"` for entries from `🔁 Reproduce`, `"fix"` for entries from `🛠️ Fix`.                                                                                                                                                                     |
| `command`          | `str`                 | The suggestion's shell-ready command string.                                                                                                                                                                                                       |
| `description`      | `str`                 | Optional short label rendered as a `# <description>` comment above the command. Empty when the task emits a single suggestion of its kind.                                                                                                         |
| `slug`             | `str`                 | **Stable kebab-case identifier** — the recommended scoping key. See [suggestion slug catalogue](#suggestion-slug-catalogue) below.                                                                                                                 |
| `status`           | `str`                 | Lifecycle status the task is about to emit: `"running"`, `"failing"`, `"passed"`, `"warning"`, `"failed"`, `"aborted"`.                                                                                                                            |
| `exit_code`        | `int`                 | Task exit code. Only meaningful on the terminal emit; `0` mid-task.                                                                                                                                                                                |
| `bazel_subcommand` | `str`                 | `"build"`, `"test"`, or `"run"` for Bazel-wrapping tasks; `""` otherwise.                                                                                                                                                                          |
| `targets`          | `list[str]`           | Resolved target patterns the task ran on.                                                                                                                                                                                                          |
| `failed_targets`   | `list[str]`           | Deduplicated list of Bazel labels that failed. Empty on non-Bazel tasks.                                                                                                                                                                           |
| `extras`           | `dict`                | Open-ended, task-stamped, curated keys. Documented per-task. Examples: `lint` stamps `{"aspects": [...], "suggestion_count": N}`; `format` and `gazelle` stamp `{"affected_files": [...]}`; `delivery` stamps `{"mode": str, "targets_total": N}`. |
| `data`             | `dict`                | **Escape hatch** — full task `data` dict for hooks that need fields not surfaced by the typed fields or `extras`. Treat as **unstable**; prefer the typed surfaces above.                                                                          |

## Suggestion slug catalogue

Every suggestion carries a stable kebab-case `slug` so hooks scope by suggestion identity instead of parsing the command string. The full catalogue:

| Task                       | Slugs                                                                                                          |
| -------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `lint`                     | `lint-repro`, `lint-fix`                                                                                       |
| `format`                   | `format-repro`, `format-fix`, `format-fix-truncated`, `format-fix-rediscover`, `format-fix-vanilla-bazel`      |
| `gazelle`                  | `gazelle-repro`, `gazelle-fix`, `gazelle-fix-truncated`, `gazelle-fix-rediscover`, `gazelle-fix-vanilla-bazel` |
| `delivery`                 | `delivery-repro-local-preview`                                                                                 |
| `bazel` (`build` / `test`) | `bazel-repro-aspect`, `bazel-repro-vanilla-bazel`                                                              |

**Convention:** every vanilla-`bazel` alternate suggestion ends in `-vanilla-bazel`, so a single hook can suffix-match to scope across all producers without listing each slug.

## Examples

Three production-grade examples, all currently shipping in the [`aspect-build/aspect-cli` repo's own `.aspect/config.axl`](https://github.com/aspect-build/aspect-cli/blob/main/.aspect/config.axl).

### Rewrite `aspect …` to `bazel …` for one task

Useful when your repo has a [`tools/bazel`](/docs/cli/install#keep-your-team-typing-bazel-with-the-tools%2Fbazel-wrapper) wrapper and you want suggestions to use the `bazel` form so new contributors copy-paste a command that goes through the checked-in Bazel version.

```python title=".aspect/config.axl" theme={null}
def _buildifier_repro_fix(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    """Rewrite buildifier `aspect ...` suggestions to `bazel ...`."""
    if info.task_kind != "buildifier":
        return REPRO_FIX_ACCEPT
    return repro_fix_replace(
        command = info.command.replace("aspect buildifier", "bazel buildifier", 1),
    )

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_buildifier_repro_fix)
```

### Drop the vanilla-bazel alternate everywhere except one task

Built-in tasks tack on a vanilla `bazel run <target>` suggestion next to their `aspect …` repros/fixes — a fallback for environments where the Aspect CLI isn't on PATH. If your team always has `aspect` (or a [`tools/bazel`](/docs/cli/install#keep-your-team-typing-bazel-with-the-tools%2Fbazel-wrapper) wrapper that routes through it), those alternates are noise.

```python title=".aspect/config.axl" theme={null}
def _drop_vanilla_bazel_except_build(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    """Drop every vanilla-bazel alternate except `build`'s."""
    if not info.slug.endswith("-vanilla-bazel") or info.task_kind == "build":
        return REPRO_FIX_ACCEPT
    return REPRO_FIX_REJECT

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_drop_vanilla_bazel_except_build)
```

The `endswith("-vanilla-bazel")` suffix match catches every current producer (`format`, `gazelle`, `build`, `test`) and any new ones added later.

### Rewrite a description without touching the command

`repro_fix_replace` takes either field independently — passing only `description` keeps the existing `command` unchanged.

```python title=".aspect/config.axl" theme={null}
def _shorten_delivery_local_preview(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    """Shorten the delivery local-preview description for consistency across runs."""
    if info.slug != "delivery-repro-local-preview":
        return REPRO_FIX_ACCEPT
    return repro_fix_replace(
        description = "--mode=always --track-state=false for off-runner with no state backend.",
    )

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_shorten_delivery_local_preview)
```

## Order matters

Hooks run in **registration order**, and each `replace` verdict feeds the next hook in the chain. So if you register a rewrite hook and a reject hook, register the rewrite **first** — otherwise the reject may match on the original slug and short-circuit before the rewrite runs (or worse, on a rewritten slug the next hook can no longer recognize).

The slug is preserved across `replace` verdicts (hooks rewrite commands and descriptions, not suggestion identity), so slug-scoped hooks downstream of a `replace` still match correctly. But the rewritten `command` / `description` is what downstream hooks see.

## How it fits into the lifecycle

`apply_repro_fix_hooks` runs automatically on every surface emit via the framework's `dispatch_task_update` — no surface (CLI printer, Buildkite annotation, GitHub Status Check body, PR-comment rollup) ever renders an un-hooked entry. An idempotence flag on each `ReproFixCommand` makes repeated dispatches safe; each entry runs through the chain at most once even though every emit dispatches.

Hooks see the same suggestions every consumer sees, in the same order, so a rewrite or rejection flows through to every surface in lock-step.

## See also

* [How to run and define tasks](/docs/cli/guides/basic) — task fundamentals and the `task()` definition surface.
* [Aspect Extension Language overview](/docs/cli/overview#aspect-extension-language) — what AXL is and why it's typed Starlark.
* [`tools/bazel` wrapper](/docs/cli/install#keep-your-team-typing-bazel-with-the-tools%2Fbazel-wrapper) — pairs naturally with the rewrite-to-`bazel` hook in the first example.
