Skip to content

Batch edit

batch_edit

BatchEditTool — atomic batch file editing for AI agents.

Registered as batch_edit via the axm.tools entry point.

BatchEditTool

Atomic batch file editing for AI agents.

Replaces, rewrites, creates, and deletes files in a single atomic operation. Registered as batch_edit via axm.tools entry point.

Source code in packages/axm-edit/src/axm_edit/tools/batch_edit.py
Python
class BatchEditTool:
    """Atomic batch file editing for AI agents.

    Replaces, rewrites, creates, and deletes files in a single atomic
    operation.
    Registered as ``batch_edit`` via axm.tools entry point.
    """

    expose_directly = True
    domain = "edit"
    tags = frozenset({"edit", "atomic", "refactor"})

    agent_hint: str = (
        "Apply multiple file operations atomically."
        " Safer than sed — validates before writing."
        " A batch that fails mid-apply restores every touched file on its"
        " own; to undo a batch that succeeded, use git.\n"
        f"{OPERATION_SHAPES_HINT}\n"
        f"{ANCHOR_RULES_HINT}"
    )

    @property
    def name(self) -> str:
        """Tool name used for MCP registration."""
        return "batch_edit"

    def execute(
        self,
        *,
        path: str = ".",
        operations: list[dict[str, object]] | None = None,
        lint: bool = True,
        lint_diff: bool = True,
        lint_diff_max_ratio: float = 0.5,
        **kwargs: object,
    ) -> ToolResult:
        """Execute a batch of file operations atomically.

        Args:
            path: Project root directory.
            operations: List of operation dicts with ``op`` discriminator —
                ``replace``, ``create``, ``delete`` or ``rewrite`` (a
                whole-file replacement carrying the exact bytes, guarded by
                the ``expected_checksum`` of the current ones).
            lint: Run ruff --fix on changed Python files after apply.
            lint_diff: Surface per-file diffs of post-lint mutations.
            lint_diff_max_ratio: Fallback threshold (diff / file size).

        Returns:
            ToolResult with applied counts, the ``checkpoint`` snapshot
            payload (a JSON string, not a SHA) to pass back to
            ``batch_rollback``, and the ``preflight`` report (diagnostics,
            warnings and blocking verdict). A blocking preflight refuses the
            batch before any checkpoint or write, so the payload then
            carries no checkpoint at all.
        """
        raw_operations: list[dict[str, object]] = [
            _normalised_rewrite(raw) for raw in operations or []
        ]

        if not raw_operations:
            return ToolResult(
                success=False,
                error="No operations provided",
            )

        try:
            root = Path(path).resolve()
            if not root.is_dir():
                return ToolResult(
                    success=False,
                    error=f"Path is not a directory: {path}",
                )
            report = _preflight(root, raw_operations)
            if report.blocking:
                return _blocked_result(report)
            return _run_batch(
                root,
                _prepare_operations(raw_operations),
                report,
                _LintOptions(
                    enabled=lint,
                    diff=lint_diff,
                    diff_max_ratio=lint_diff_max_ratio,
                ),
            )
        except (OSError, ValueError, TypeError) as exc:
            return ToolResult(success=False, error=str(exc))
name property

Tool name used for MCP registration.

execute(*, path='.', operations=None, lint=True, lint_diff=True, lint_diff_max_ratio=0.5, **kwargs)

Execute a batch of file operations atomically.

Parameters:

Name Type Description Default
path str

Project root directory.

'.'
operations list[dict[str, object]] | None

List of operation dicts with op discriminator — replace, create, delete or rewrite (a whole-file replacement carrying the exact bytes, guarded by the expected_checksum of the current ones).

None
lint bool

Run ruff --fix on changed Python files after apply.

True
lint_diff bool

Surface per-file diffs of post-lint mutations.

True
lint_diff_max_ratio float

Fallback threshold (diff / file size).

0.5

Returns:

Type Description
ToolResult

ToolResult with applied counts, the checkpoint snapshot

ToolResult

payload (a JSON string, not a SHA) to pass back to

ToolResult

batch_rollback, and the preflight report (diagnostics,

ToolResult

warnings and blocking verdict). A blocking preflight refuses the

ToolResult

batch before any checkpoint or write, so the payload then

ToolResult

carries no checkpoint at all.

Source code in packages/axm-edit/src/axm_edit/tools/batch_edit.py
Python
def execute(
    self,
    *,
    path: str = ".",
    operations: list[dict[str, object]] | None = None,
    lint: bool = True,
    lint_diff: bool = True,
    lint_diff_max_ratio: float = 0.5,
    **kwargs: object,
) -> ToolResult:
    """Execute a batch of file operations atomically.

    Args:
        path: Project root directory.
        operations: List of operation dicts with ``op`` discriminator —
            ``replace``, ``create``, ``delete`` or ``rewrite`` (a
            whole-file replacement carrying the exact bytes, guarded by
            the ``expected_checksum`` of the current ones).
        lint: Run ruff --fix on changed Python files after apply.
        lint_diff: Surface per-file diffs of post-lint mutations.
        lint_diff_max_ratio: Fallback threshold (diff / file size).

    Returns:
        ToolResult with applied counts, the ``checkpoint`` snapshot
        payload (a JSON string, not a SHA) to pass back to
        ``batch_rollback``, and the ``preflight`` report (diagnostics,
        warnings and blocking verdict). A blocking preflight refuses the
        batch before any checkpoint or write, so the payload then
        carries no checkpoint at all.
    """
    raw_operations: list[dict[str, object]] = [
        _normalised_rewrite(raw) for raw in operations or []
    ]

    if not raw_operations:
        return ToolResult(
            success=False,
            error="No operations provided",
        )

    try:
        root = Path(path).resolve()
        if not root.is_dir():
            return ToolResult(
                success=False,
                error=f"Path is not a directory: {path}",
            )
        report = _preflight(root, raw_operations)
        if report.blocking:
            return _blocked_result(report)
        return _run_batch(
            root,
            _prepare_operations(raw_operations),
            report,
            _LintOptions(
                enabled=lint,
                diff=lint_diff,
                diff_max_ratio=lint_diff_max_ratio,
            ),
        )
    except (OSError, ValueError, TypeError) as exc:
        return ToolResult(success=False, error=str(exc))

render_text(result, parsed, data)

Render a compact, git-style LLM-facing view of a batch result.

The header carries the global status — ✓ on success, or on failure ✗ with a label naming what happened to the tree (see :func:_failure_label), so a partial write is impossible to miss — followed by the modified/created/deleted/edits counts. Each operated-on file is then listed with its op sigil and edit count, every validation error is surfaced verbatim, and the full lint summary (fixes, remaining errors, warnings, diffs) is appended.

One key is deliberately absent: data["checkpoint"] carries the base64 snapshot of every touched file before the batch, measured in tens of thousands of tokens on a real batch. It stays in data for batch_rollback and never reaches this view. Everything else in data is rendered; only its JSON structure is dropped.

Source code in packages/axm-edit/src/axm_edit/tools/batch_edit.py
Python
def render_text(
    result: BatchResult,
    parsed: list[Operation],
    data: dict[str, object],
) -> str:
    """Render a compact, ``git``-style LLM-facing view of a batch result.

    The header carries the global status — ``✓`` on success, or on failure
    ``✗`` with a label naming what happened to the tree (see
    :func:`_failure_label`), so a partial write is impossible to miss —
    followed by the modified/created/deleted/edits counts. Each
    operated-on file is then listed with its op sigil and edit count, every
    validation error is surfaced verbatim, and the full lint summary
    (fixes, remaining errors, warnings, diffs) is appended.

    One key is deliberately absent: ``data["checkpoint"]`` carries the
    base64 snapshot of every touched file *before* the batch, measured in
    tens of thousands of tokens on a real batch. It stays in ``data`` for
    ``batch_rollback`` and never reaches this view. Everything else in
    ``data`` is rendered; only its JSON structure is dropped.
    """
    alerts = _render_import_alerts(data)
    preflight = _render_preflight_lines(data)
    if result.success:
        s = result.summary
        header = (
            f"batch_edit | ✓ | {s.get('modified', 0)} modified"
            f" · {s.get('created', 0)} created · {s.get('deleted', 0)} deleted"
            f" · {result.applied} edit{'s' if result.applied != 1 else ''}"
        )
        lines = [
            *alerts,
            header,
            *_render_op_lines(parsed),
            *preflight,
            *_render_lint_lines(data),
        ]
        return "\n".join(lines)

    label = _failure_label(result, data)
    header = f"batch_edit | ✗ {label} | {result.error or 'failed'}"
    lines = [
        *alerts,
        header,
        *_render_op_lines(parsed),
        *preflight,
        *_render_error_lines(result),
        *([_ROLLBACK_FAILED_LINE] if result.rollback_failed else []),
    ]
    return "\n".join(lines)