# Usage

`atomic-edit` overrides the built-in `edit` tool at load time. The model still
calls `edit`; no new tool name is introduced. Its behavior has two independent
axes:

| Axis | Options |
|---|---|
| **matching** | `sequential` (default) — each `edits[i]` is matched against the file *after* `edits[i-1]` is applied, so chained/overlapping edits work.<br>`snapshot` — every edit is matched against the *original* file (pi's built-in semantics; edits must be disjoint). |
| **writing** | `atomic` (default) — write to a temp file in the same directory, `fsync`, then `rename` over the target (atomic; no torn files, direct-write fallback).<br>`direct` — plain `writeFile` (pi's built-in; keeps the inode). |

## Install modes

### As a pi package (recommended)

```bash
pi install npm:atomic-edit      # into ~/.pi/agent/settings.json
pi -e npm:atomic-edit           # one-off session
pi install git:gitlab.com/openbracket/atomic-edit@v0.2.2
pi install ./atomic-edit        # local checkout
```

### As a directory (development)

A checkout can also be loaded directly, without the package machinery:

```bash
# one session only
pi --extension /path/to/atomic-edit

# or as a user extension
ln -s /path/to/atomic-edit ~/.pi/agent/extensions/atomic-edit
```

`pi` loads the directory via its `index.ts`; the other `.ts` files are not
loaded separately.

### Disable

```bash
# package install
pi remove npm:atomic-edit

# directory install
mv ~/.pi/agent/extensions/atomic-edit ~/.pi/agent/extensions/atomic-edit.off
```

Do not load both at once: the package and the directory would register `edit`
and the `--snapshot`/`--no-atomic` flags twice.

## Turning modes on and off

Three ways, with **later sources winning**:

```
/edit-mode (live toggle)  >  --flag  >  PI_* env var  >  default
```

### 1. CLI flags

```bash
pi                        # sequential + atomic (both fixes on)
pi --snapshot             # built-in matching + atomic write
pi --no-atomic            # sequential matching + built-in write
pi --snapshot --no-atomic # built-in behavior (same file contents)
```

### 2. `/edit-mode` (live, no restart)

```
/edit-mode                 # show current mode + usage
/edit-mode snapshot        # switch matching
/edit-mode direct          # switch write
/edit-mode sequential atomic
/edit-mode toggle          # flip matching (snapshot <-> sequential)
/edit-mode reset           # re-resolve from --flags and PI_* env vars
```

The current mode is shown in the footer status as `edit:<seq|snap>/<atom|direct>`.

### 3. Environment variables

Useful for scripts that cannot pass flags:

```bash
PI_EDIT_SNAPSHOT=1    # same as --snapshot
PI_EDIT_NO_ATOMIC=1   # same as --no-atomic
PI_EDIT_FSYNC=0       # keep atomic replace, skip fsync (faster; weaker crash safety)
```

Accepted truthy values: `1`, `true`, `yes` (case-insensitive).
Accepted falsy values (for `PI_EDIT_FSYNC`): `0`, `false`, `no`.

### Behavior matrix

| Matching | Write | Equivalent to |
|---|---|---|
| sequential | atomic | both tweaks |
| snapshot | atomic | built-in matching, atomic write |
| sequential | direct | built-in write, chained edits allowed |
| snapshot | direct | built-in `edit` (see the uniqueness caveat below) |

## Tool compatibility

The override keeps the built-in's argument coercion and path handling: `edits`
may be an array, a JSON string, or a single edit object, the legacy top-level
`oldText`/`newText` form is accepted, and paths expand `~`, strip a leading `@`,
normalize unicode spaces, and accept `file://` URLs. The prompt snippet,
guidelines, constrained-sampling hint, and built-in renderers are preserved, so
overriding `edit` does not change the model-facing tool contract.

## How matching works

- Exact `indexOf` first; on miss, one fuzzy pass that:
  - strips **trailing** whitespace per line,
  - NFKC-normalizes,
  - folds smart quotes, unicode dashes and special spaces to ASCII.
  - Leading indentation and line-count changes are **not** tolerated.
- Matches must be unique. Uniqueness is checked in the coordinate space of the
  match: an exact match counts exact occurrences, a fuzzy match counts
  fuzzy-normalized occurrences. The built-in always counts fuzzy-normalized
  occurrences, so `--snapshot` can accept an exact-unique edit that the built-in
  rejects as ambiguous after quote/NBSP/trailing-whitespace normalization. It
  never rejects a call the built-in accepts, and when both accept the resulting
  file is byte-for-byte identical.
- In **sequential** mode, each step runs against the evolving buffer; fuzzy
  replacements rewrite only the touched lines and copy every other line back
  byte-for-byte.
- In **snapshot** mode, all edits are matched against the original and must not
  overlap (overlap throws).

Sequential matching means edits are **not** idempotent in the way snapshot
matching is; an edit can match text created by an earlier edit in the same call.

## How writing works

`atomicWriteFile`:

```
realpath target (preserve symlinks)
-> open exclusive temp file in the same dir (short random name, mode = original)
-> write -> fsync file              (skipped when PI_EDIT_FSYNC=0)
-> chown/chmod best-effort (preserve owner + permissions)
-> rename(tmp, target)              (atomic; retries EBUSY/EPERM/EACCES)
-> fsync directory (best-effort)    (make the rename durable)
-> unlink temp on any failure
-> fall back to a direct write if the temp file or rename is refused
```

Both modes read once, apply all edits in memory, and write once, so a failed
later edit leaves the file untouched.

`atomicWriteFile` falls back to a direct `writeFile` instead of failing the edit
when the platform refuses the temp file or rename: read-only directory where
the target itself is writable, name-length limits, unsupported filesystem
operations. Transient rename locks (Windows sharing violations, antivirus scans)
are retried a few times first. A fallback keeps the inode but takes the
direct-write tradeoffs below, and the tool result says `write=direct via
fallback`.

The short temp name (`.pi-edit-<pid>-<random>.tmp`) avoids `ENAMETOOLONG` for
basenames near `NAME_MAX`. Leftover temp files are cleaned up on failure; a
`SIGKILL` between create and rename can still leave one behind.

### Atomic vs direct write

| | atomic (temp + rename) | direct (`writeFile`) |
|---|---|---|
| Crash / full disk mid-write | old file intact | **can truncate** |
| Inode | changes | preserved |
| Hard links | broken | preserved |
| Permissions/owner | copied (best effort) | preserved |
| Symlinks | link preserved (target rewritten) | preserved |
| ACLs / xattrs | not carried over | preserved |
| File watchers | create + rename events | modify event |
| macOS metadata | tags, quarantine, resource forks, ACLs, `chflags`, "Date Added" are lost | preserved |

## Performance notes

Exact-match and fuzzy-match workloads behave differently:

- **Exact matches (both modes).** Each edit is one `indexOf` scan of the current
  buffer plus a uniqueness scan of the remainder, with no whole-file
  normalization and no substring allocation. Total cost is linear in
  `edits × file size`.
- **Snapshot + fuzzy.** The file is normalized once per call, then every edit is
  matched against that normalized copy; replacements are applied in one forward
  pass, so many edits do not multiply copies of the buffer.
- **Sequential + fuzzy.** The default mode re-normalizes the whole buffer for
  each fuzzy edit, because each edit is matched against the buffer left by the
  previous one. Cost is linear per edit but with a much larger constant
  (~9 ms per 3.8 MB normalize on the development machine). Prefer fuzzy-free
  edits for large files, or use `--snapshot` for disjoint fuzzy edits.
- **Snapshot + exact.** The first matching pass is reused when no edit needed
  the fuzzy fallback, so the file is not scanned twice per edit.

Other costs:

- Sequential mode copies the buffer once per edit (required by chained
  semantics). Snapshot mode copies it once per call.
- The file is read once and written once. The atomic path adds a `stat`, temp
  file create, `rename`, and permission work: ~0.2 ms for small files without
  `fsync`.
- `fsync` dominates per-edit latency on local disks (~9.2 ms for a 4 KB file on
  the development machine). Use `PI_EDIT_FSYNC=0` only if the weaker crash
  guarantee is acceptable.
- Duplicate detection stops after finding a second occurrence; successful unique
  matches still scan the rest of the file.
- Diff + patch generation is pi's own helper, runs after the write, and is the
  remaining CPU cost on very large files. It is kept for result-shape
  compatibility with the built-in `edit`.

See the [README](../README.md#speed) for measured numbers and
[development.md](development.md) for how to reproduce them.

## Caveats

- Not an upstream patch: it overrides `edit` at load time and will not survive a
  pi reinstall, but nothing under pi's `dist/` is modified.
- The atomic path changes the inode. If you rely on hard links, ACLs, xattrs, or
  inode-stable watchers, use `--no-atomic`.
- On macOS, `fsync` only flushes to the drive cache; Apple requires
  `F_FULLFSYNC` for real durability, which Node does not expose. Directory
  fsync is best-effort there. A process crash is still safe (the rename is
  atomic); a kernel panic or power loss can lose the newest write without
  corrupting the file. Use `PI_EDIT_FSYNC=0` to skip the cost when you do not
  need even that.
- Editing a read-only file in a writable directory succeeds with the atomic
  writer (rename only needs directory write permission) where the built-in
  would fail. The file's mode is preserved.
- `--snapshot` can accept a few calls the built-in rejects as fuzzy-ambiguous;
  see [How matching works](#how-matching-works).
