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 edits atomically via op=replace"
        " with old/new pairs. Safer than sed — validates before writing.\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 ✗ ROLLBACK on failure so a partial/aborted batch 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. Nothing in data is dropped; only its JSON structure is.

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
    ``✗ ROLLBACK`` on failure so a partial/aborted batch 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. Nothing in
    ``data`` is dropped; only its JSON structure is.
    """
    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 = "PREFLIGHT" if _preflight_blocking(data) else "ROLLBACK"
    header = f"batch_edit | ✗ {label} | {result.error or 'failed'}"
    lines = [
        *alerts,
        header,
        *_render_op_lines(parsed),
        *preflight,
        *_render_error_lines(result),
    ]
    return "\n".join(lines)