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, 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, 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."
    )

    @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.
            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 and the ``checkpoint`` snapshot
            payload (a JSON string, not a SHA) to pass back to
            ``batch_rollback``.
        """
        raw_operations: list[dict[str, object]] = operations or []

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

        try:
            parsed = _parse_operations(raw_operations)
        except (ValueError, TypeError) as exc:
            return ToolResult(success=False, error=f"Invalid operations: {exc}")

        try:
            root = Path(path).resolve()
            if not root.is_dir():
                return ToolResult(
                    success=False,
                    error=f"Path is not a directory: {path}",
                )

            return _run_batch(
                root,
                parsed,
                lint=lint,
                lint_diff=lint_diff,
                lint_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.

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 and the checkpoint snapshot

ToolResult

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

ToolResult

batch_rollback.

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.
        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 and the ``checkpoint`` snapshot
        payload (a JSON string, not a SHA) to pass back to
        ``batch_rollback``.
    """
    raw_operations: list[dict[str, object]] = operations or []

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

    try:
        parsed = _parse_operations(raw_operations)
    except (ValueError, TypeError) as exc:
        return ToolResult(success=False, error=f"Invalid operations: {exc}")

    try:
        root = Path(path).resolve()
        if not root.is_dir():
            return ToolResult(
                success=False,
                error=f"Path is not a directory: {path}",
            )

        return _run_batch(
            root,
            parsed,
            lint=lint,
            lint_diff=lint_diff,
            lint_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.
    """
    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 = [header, *_render_op_lines(parsed), *_render_lint_lines(data)]
        return "\n".join(lines)

    header = f"batch_edit | ✗ ROLLBACK | {result.error or 'failed'}"
    lines = [header, *_render_op_lines(parsed)]
    for d in result.details:
        suffix = f" [expected: {d.expected}]" if d.expected else ""
        loc = f"{d.file}:{d.line}" if d.line is not None else d.file
        lines.append(f"  ! {loc}: {d.error or 'validation error'}{suffix}")
    return "\n".join(lines)