atomwrite 0.1.37

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
[Leia em Português](ARCHITECTURE.pt-BR.md)


# Architecture


## Overview
- atomwrite is a single Rust binary CLI for atomic file operations
- Designed for LLM agents that need safe, structured file manipulation
- Every write is atomic: tempfile, fsync, rename, fsync directory
- Every response is NDJSON on stdout with BLAKE3 checksums
- All diagnostic logs go to stderr via `tracing` (`init_tracing` in `src/runtime.rs`, no product telemetry): non-blocking writer, filter from CLI `-v`/`-q`, optional XDG state log file (key `log.file`), no product `ATOMWRITE_*` / `RUST_LOG` knobs in v0.1.35+
- The agent data plane remains NDJSON on stdout


## Module Map

### Entry Point
- `src/main.rs` — binary entry: signal setup, tracing init, dispatch
- `src/lib.rs` — library root: module declarations and the `run()` dispatcher
- `src/lib_dispatch.inc.rs` — command dispatch and `startup_wal_heal`, the startup `wal-heal` pass that runs before dispatch, only for verbs that mutate and only over the directories named in argv
- `src/runtime.rs` (+ `runtime_cli.inc.rs`) — binary startup helpers: tracing, locale, clap error enrichment, schema prescan
- `src/cli.rs` (+ `cli_global.inc.rs`) — clap `#[derive(Parser)]` with the global flags
- `src/cli_args/` — per-subcommand argument structs and value enums, split in `mod.rs` and `part*.inc.rs`

### Core Pipeline
- `src/atomic/` — atomic write pipeline: `write.rs` orchestrates tempfile + fsync + rename + fsync dir, `rename_path.rs` is the tempfile path, `inplace.rs` holds the in-place write, the EXDEV fallback and timestamped backups, `persist_retry.rs` retries the Windows tempfile persist, `types.rs` and `time_fmt.rs` are helpers
- `src/checksum.rs` — BLAKE3 hash computation for files and byte slices (uses memmap2 for large files)
- `src/file_io.rs` — smart file reading: one open, one `fstat` on the handle, one read into an exact-size buffer (`try_reserve_exact`), hashing uses `checksum::hash_file`
- `src/platform.rs` — platform durability and rename: `Durability` full|fast|auto, Linux `renameat2` with rename fallback, macOS `F_FULLFSYNC` on full, and the write NDJSON reports `platform.durability` and `platform.rename_method`
- `src/fuzzy/` — shared match cascade used by edit, replace, batch and edit-loop: exact, `line_trimmed`, `whitespace_normalized`, `punctuation_normalized`, `indent_flexible`, `trimmed_boundary`, `escape_normalized`, `block_anchor`, `unicode_normalized` and `context_aware` (with a Jaro-Winkler pass)
- `src/fuzzy/one_pass.inc.rs` — one-pass multi-apply (`apply_fuzzy_one_pass`) on the original content, never re-scanning inserted text, a pattern embedded in its replacement forces a single apply, default max applies = 1, hard ceiling 10_000, cooperative cancel polled mid-cascade, caps (pattern 64 KiB, Levenshtein 8192 chars, windows 4096, growth max(4x, +16 MiB))
- The fuzzy output carries `match_count` and `indent_adjusted` in `EditOutput`, normalized strategies emit `similarity: null` (not a fake 1.0), `block_anchor` dual-gates the middle ratio, and `context_aware` applies a soft floor via XDG `[fuzzy].soft_floor_delta` (default 0.05)
- Reflink (copy-on-write) lives in `src/atomic/write.rs` and `src/atomic/inplace.rs`, through the `reflink-copy` crate

### Safety and Validation
- `src/path_safety.rs` — workspace jail: path traversal prevention, symlink validation, FIFO/device detection, Windows reserved names (COM0–9/LPT0–9), NFC, long-path `\\?\` helper
- `src/storage.rs` — config/data/cache/state via `directories::ProjectDirs` (XDG), no product `ATOMWRITE_HOME` runtime knob in v0.1.35+
- `src/env_detect.rs` — WSL/container/K8s/Termux/Flatpak/Snap autodetection for `doctor`, from file probes only (no environment variable is read)
- `src/signal.rs` (+ `signal_handlers.inc.rs`, `signal_cancel.inc.rs`) — SIGINT/SIGTERM/SIGHUP and the SIGPIPE handler via signal-hook with graceful shutdown coordination, and `CancelCheck` for the cooperative cancel
- `src/stdin_cancel.rs` — cancellable stdin: a reader thread that gives up when the run is cancelled or the deadline passes
- `src/error/` — domain error enum with exit codes, error classification and the retryable flag, split in `variants.inc.rs`, `class.inc.rs`, `impl_methods.inc.rs`, `error_json.inc.rs` and `suggest.inc.rs`
- `src/lock.rs` — advisory file locking via `std::fs::File::try_lock`, which is `flock` on Unix and `LockFileEx` on Windows, on the `.<target>.atomwrite.lock` sidecar, and the sidecar name becomes a short BLAKE3 digest when the literal name would pass 255 bytes
- `src/walk_globs.rs` — the one place that compiles the `--include` / `--exclude` globs of every verb, a glob that does not compile is `INVALID_INPUT` (exit 65)

### Crash Recovery (v0.1.12, G114 + v0.1.15-v0.1.18, G119)
- `src/wal/` — the WAL sidecar, one module per concern
- `src/wal/journal.rs` — sidecar writer: appends `Started` and `Committed` entries to `.atomwrite.journal.<target>.atomwrite.journal.json`, and the `JournalGuard` RAII from G119 L2
- `src/wal/policy.rs` — `WalPolicy` (`Auto`, `Always`, `Never`) and the creation heuristics, including `creation_throttled`, the H3 rate limit applied only when a journal is about to be created
- `src/wal/heuristics.rs` — G119 L4 HeuristicsEngine: `h1_ttl`, `h2_lru_within_cap`, `h3_rate_limit`, `h4_sentinel`, `h5_archive`, where `heuristics_should_preserve` aggregates the retention votes for a committed journal and does not consult `h3_rate_limit`
- `src/wal/heal.rs` — `recover_orphan_journals(dir)`, `auto_heal_scoped` and the `wal-heal` pass, which removes `Committed` and `Aborted` sidecars, and removes a `Started` sidecar only when the owning PID no longer exists, reported as `dead_owner_removed`
- `src/wal/stats.rs` and `src/commands/wal_stats.rs` — G119 L5 `wal-stats` subcommand: total_journals, by_state, oldest_age, total_size, by_directory, auto_heal_recommended, estimated_reclaim_bytes

### Syntax Check (v0.1.12, G72)
- `src/syntax_check.rs` — REAL tree-sitter syntax check via `tree-sitter-language-pack`, replacing the v0.1.11 bracket-balance heuristic, with 23 languages mapped by file extension
- Unknown extensions fall back to the legacy heuristic in `src/atomic/syntax_heuristic.rs`
- `src/syntax_check_stub.rs` — the stub used when the `ast` feature is off

### Output
- `src/output.rs` (+ `output_lines.inc.rs`, `output_verdict.inc.rs`) — NDJSON writer with broken-pipe handling, and the success/failure stamp of the terminal event
- `src/output_shape.inc.rs` and `src/output_shape/` — the global runtime shape applied at emission (`--select`/`--fields`, `--filter`, `--limit`, `--sort-by`, `--dedupe`, `--aggregate`, `--join`, `--diff-against`, `--redact`), these are not schema fields
- `src/ndjson/` — output type definitions with schemars JSON Schema derivation
- `src/schema_dispatch.inc.rs` and `src/schema_terminal.inc.rs` — the `--print-schema` listing and the closed terminal schemas
- `src/error/` — the error envelope always emits `ok: false` (required) plus `match_count` on match failures (0 on most failures), with an optional `best_candidate` on miss/ambiguity
- `src/constants.rs` — named constants for buffer sizes, thresholds and exit codes

### Utilities
- `src/binary_detect.rs` — null-byte heuristic for binary content detection
- `src/line_endings.rs` — LF/CRLF/CR detection and normalization
- `src/lang_utils.rs` — programming-language extension maps for AST commands
- `src/locale.rs` (+ `locale_persist.inc.rs`) — UI locale: `Language`, sys-locale detect, unic-langid parse, fluent-langneg negotiate, OnceLock, XDG preference file
- `src/xattr_restore.rs` — save and restore xattrs (macOS quarantine, Linux selinux/capabilities)
- `src/concurrency.rs` (+ `concurrency_walk.inc.rs`) — bounded concurrency and multi-core fan-out, `WalkParallel` collection
- `src/config.rs` — loading of `.atomwrite.toml` and the XDG `config.toml` (flags > workspace > global > defaults)

### Reliability, Tuning and Failure Reporting (v0.1.37)
- `src/retry.rs` — retry with exponential backoff, full jitter and a hard deadline (`RetryPolicy`, `Backoff`, `retry()`), used by the advisory lock and the Windows tempfile persist, sleeping in short slices and obeying the global `--timeout-secs` deadline and the shutdown flag
- `src/best_effort.rs` — the sanctioned way to ignore a failure on purpose: `best_effort` logs at `debug` and continues, while a failure that changes the result is propagated with `?`
- `src/config_tuning.inc.rs` — tuning sections (`concurrency`, `search`, `lock`, `retry`, `extract`), their validation and the process-wide `tuning()` accessor
- `src/commands/config_cmd.rs` (+ `config_cmd_keys.inc.rs`, `config_cmd_write.inc.rs`) — the `config` verb: one typed table of 37 keys, layered read (flag, workspace, global, built-in), validated atomic write that keeps comments
- `src/constants.rs` — includes `constants_concurrency.inc.rs` (retry, thread ceiling, search budget), `constants_config.inc.rs` (config file names, key minimums) and `constants_literals.inc.rs` (named literals: sidecar names, time units, sizes)
- `src/commands/search_budget.inc.rs` — one global byte budget for the search events in flight (default 8 MiB, key `search.max_inflight_bytes`), and the stream being drained never waits, so it cannot deadlock
- `src/ndjson/part3.inc.rs` — `FileFailure`, `FailureSummary`, `FailureTally` and `partial_of`: `files_failed`, `partial` and `errors[]` (at most 50) on the terminal summary of every verb that walks a tree, with `partial` derived from the same count
- `src/output_verdict.inc.rs` — success/failure stamping of the terminal event and the `VerdictPublished` marker, so a failing summary is the only error line of the run
- `workspace_source` — every terminal envelope publishes `argv` or `cwd`, so a run that took the root from the process working directory says so instead of hiding the ambient authority
- `replace --expect-checksum` — the comparison runs under the lock and a drift exits 82 `STATE_DRIFT`, closing the window between the check and the write
- `batch --transaction` — every target lock is taken in path order before the snapshot and held until commit or rollback, so two batches wait instead of interleaving
- `edit-loop` — the checksum read under the lock is the write base, so a concurrent edit exits 82 instead of a lost update
- Typed errors — the edge paths of `cli.rs`, `rename_path.rs` and the fsync helpers raise `AtomwriteError`, so a denied directory exits 13 `PERMISSION_DENIED` and never 255 `INTERNAL_ERROR`
- `src/commands/replace_file.inc.rs` and `src/commands/replace_finish.inc.rs` — the per-file step and the closing step of `replace`, split out of `replace_run.inc.rs`
- `src/walk_globs.rs`, `src/stdin_cancel.rs` and `src/signal_cancel.inc.rs` — the shared glob compiler, the cancellable stdin and the cancel helpers listed above

### Subcommand Handlers
- `src/commands/` — 47 subcommand implementations (handlers + aliases, SSOT `CANONICAL_SUBCOMMAND_COUNT` / `agent-surface`), each in its own module where applicable, and `batch/`, `edit/`, `extract_artifact/` and `scope/` are folders
- Each handler receives parsed args, global config, an NDJSON writer, and the shutdown signal
- All handlers follow the same signature: `fn cmd_*(args, global, writer, shutdown) -> Result<()>`
- v0.1.11 baseline (22): read, write, edit, search, replace, hash, delete, count, diff, move, copy, list, extract, calc, regex, transform, scope, batch, backup, rollback, apply, completions
- v0.1.12 added (6): set, get, del, case, query, outline
- v0.1.15 added (2): wal-heal (G119 L3), wal-stats (G119 L5)
- v0.1.22 added (2): edit-loop, prune-backups
- v0.1.25 added (1): verify
- v0.1.29 added (8): recipe, sparse, semantic-merge, agent-surface, watch, codemod, semantic-search, stat (alias of `read --stat`)
- v0.1.29 command modules: `recipe.rs`, `sparse.rs`, `semantic_merge.rs`, `agent_surface.rs`, `watch.rs`, `codemod.rs`, `semantic_search.rs`
- Host / discovery (complete 41 → 46): `doctor` (host env diagnostics), `locale` (resolved locale + XDG preference), `commands` (full command-tree manifest), `scratch` (`scratch init` staging under `.atomwrite/scratch/`), and `extract-artifact`
- v0.1.37 added (1): `config` (persistent tuning keys), the 47th subcommand
- Historical note: 41 was the documented surface through v0.1.35, and the current SSOT is 47

### Cargo Features (v0.1.29+)
- `core` — slim binary without AST/watch (PRD 5–8 MiB target, 10,908,040 bytes measured on 2026-10-31, DERIVADO 10908040 / 1048576 = 10.40 MiB, and ~7.7 MiB at v0.1.29)
- `ast` — ast-grep + tree-sitter + language-pack + serde-saphyr (default with lang-rust/ts/py)
- `lang-rust` / `lang-ts` / `lang-py` / `lang-go` / `lang-full` — language surface for scope/transform
- `watch` — filesystem watch via notify (`watch` subcommand), opt-in, and the default build answers `watch` with exit 78 and a rebuild hint
- `full` — default + lang-full + watch
- `semantic-search` has no feature gate: it is always compiled (offline Jaccard)
- Default features: `core`, `ast`, `lang-rust`, `lang-ts`, `lang-py`
- Slim build: `cargo build --release --no-default-features --features core`, and in it `outline`, `query`, `scope`, `transform` and `sparse outline` exit 78 with a rebuild hint (`CONFIG_INVALID`), read from `src/commands/*_stub.rs` and `src/commands/sparse.rs`, the slim build was not executed for this document
- `cargo xtask check-features` runs `cargo check --locked --no-default-features` for the `core` and `core,watch` sets and stops at the first failure (`xtask/src/main.rs`)


## Data Flow

```
stdin ──> content bytes
             │
             ├── [write/edit/apply] ──> atomic_write() ──> tempfile
             │                              │                 │
             │                              │              fsync(fd)
             │                              │                 │
             │                              │           rename(temp, target)
             │                              │                 │
             │                              │           fsync(dir)
             │                              │                 │
             │                              └──> BLAKE3 checksum
             │
             ├── [search/replace] ──> WalkParallel ──> ripgrep engine
             │                              │
             │                       crossbeam channel
             │                              │
             │                         NDJSON events
             │
             └── [read/hash/list] ──> direct fs ops ──> NDJSON events
                                                            │
                                                       stdout (NDJSON)

v0.1.12 additions:
  write/edit ──> [if --syntax-check] ──> syntax_check.rs (tree-sitter)
                          │
                          └──> SyntaxError (exit 88) if errors found
  write/edit ──> [if WAL enabled] ──> wal/journal.rs (Started entry)
                          │
                          └──> [after rename] ──> wal/journal.rs (Committed entry)
  query/outline ──> tree-sitter parse ──> iterative DFS ──> NDJSON events
  set/get/del/case ──> toml_edit / heck ──> NDJSON events

v0.1.15 additions (G119):
  wal-heal ──> scan workspace for stale .atomwrite.journal.*.json
                  │
                  └──> heuristics_should_preserve ──> reap or skip
  wal-stats ──> aggregate by_state, by_directory, age, size ──> NDJSON

v0.1.18 additions (G118 universal resolve-first):
  write/edit/copy/apply/move/rollback/set/del/case/replace ──> validate_path (jail)
                          │
                          └──> WORKSPACE_JAIL (exit 126) on first violation
```


## Key Decisions

### BLAKE3 over SHA-256
- BLAKE3 is designed to be faster than SHA-256 (upstream benchmarks, not measured here)
- Single-threaded performance matters for CLI latency
- Not used for cryptographic security, only integrity detection

### NDJSON over JSON Array
- Streaming: each result is emitted as soon as computed
- No need to buffer entire output before writing
- Pipe-friendly: downstream tools process line by line
- Errors emit in the same format with `error: true` discriminator

### tempfile + rename over In-Place Write
- Atomic: the target file is never left half-written by a rename-based write
- Behavior after SIGKILL or power loss depends on the filesystem and on `--durability`
- Backup of original is a copy (not hardlink) to avoid shared-inode corruption
- In-place fallback (v0.1.12, G55): when `nlink > 1` (Unix) or target is a symlink, atomwrite switches to `ftruncate(0) + write_all + fsync_data` to preserve the inode, and NDJSON gains `write_strategy: "rename" | "inplace" | "copyback"`

### Workspace Jail
- All paths validated against --workspace root
- Prevents path traversal via `..` components
- Blocks symlinks pointing outside the workspace
- Rejects FIFO and device files (non-atomic by nature)

### Signal Handling via signal-hook
- SIGINT and SIGTERM set an atomic flag for cooperative shutdown
- A second signal forces immediate exit via `_exit` (Unix) / `process::exit` (Windows)
- SIGPIPE is neither ignored nor left at `SIG_DFL`: a cooperative handler (`register_broken_pipe` in `src/signal_handlers.inc.rs`) marks the shutdown and the run exits 141 after it has released locks, journals and tempfiles
- This is a conscious deviation from the plain reset-to-default convention, recorded in ADR-0063, because a default-disposition kill left locks, journals and tempfiles behind
- Shared singleton `ShutdownSignal` (`OnceLock`) so polling and main-thread `is_shutdown()` see the same flag
- Cooperative cancel also covers global `--timeout-secs` (default 120, pass `0` to disable, exit 124 on deadline, GNU `timeout` convention)
- Fuzzy cascade polls the cancel flag mid-file (strategies + sliding windows) so the timeout works during `replace --fuzzy` / edit / batch / edit-loop
- `signal::cancelled_error` stamps the live exit code (130 / 143 / 124), never hard-code SIGTERM
- Long walks (`search`, `replace`, `count`, `list`, `hash`, `delete`, `batch`, …) poll the flag and stop accepting new work
- Atomic writes cancel mid-chunk without renaming the tempfile (target stays intact)
- One-shot CLI: no Tokio `CancellationToken` / `TaskTracker` / systemd `sd_notify` (N/A)

### G72 REAL tree-sitter syntax check (v0.1.12)
- Replaces the v0.1.11 bracket-balance heuristic that had false positives (Python indentation, JS template literals) and false negatives (Python `import` of missing module)
- Uses `tree-sitter-language-pack` with `download` + `dynamic-loading` features
- 23 languages are mapped by extension, and unknown extensions fall back to the legacy heuristic
- Exit 88 with first error line/column/kind/message

### G114 WAL sidecar for crash recovery (v0.1.12)
- Sidecar path: `.atomwrite.journal.<target>.atomwrite.journal.json`
- Appends `Started` (op_id, expected_new_checksum, pid, started_at_unix) and `Committed` (op_id, committed_at_unix) entries
- `recover_orphan_journals(dir)` is consultative: it reads sidecars and reports orphans without touching the FS
- The caller decides whether to replay, abort, or ignore

### tree-sitter-language-pack with dynamic-loading (v0.1.12, ADR-0019, ADR-0057)
- Parsers are NOT bundled
- They are downloaded on first use of a language and cached under `$XDG_CACHE_HOME/tree-sitter-language-pack/v1.22.1/` (`bundles`, `libs`, `manifest.json`), from the release assets of `github.com/xberg-io/tree-sitter-language-pack`, whose 1.22.1 manifest lists 372 languages
- The manifest of 1.22.1 lists 23,031,807 bytes (about 22 MiB) for the `linux-x86_64` bundle, measured on 2026-10-31, and later runs reuse the cache
- The HTTP client is `ureq`, which reads the proxy variables of the environment, and a host with no network can pre-populate the cache directory
- Alternative: `tree-sitter` as a direct dependency, but it adds hundreds of parser crates to the compile time (ADR-0019)

### v14 Tier 3 architecture (v0.1.12)
- `set/get/del` use `toml_edit` (preserves comments and key order) for TOML and `serde_json` (canonical) for JSON
- `get/del` use manual `Table` descent via the `get_toml_path` and `remove_toml_path` helpers (ADR-0024) instead of `toml_edit::Document::get`, which treats dotted keys as literal
- `case` uses the `heck` crate for 5 identifier-case styles
- `query/outline` use iterative DFS via a `Vec<Node>` stack to avoid stack overflow on deep files (compared to a recursive `TreeCursor` traversal)

### Shared fuzzy cascade for multi-pair edit (v0.1.15, G117, ADR-0026)
- `match_pair(content, old, new, fuzzy_mode)` extracts the cascade, shared by the single-pair and multi-pair paths
- Multi-pair runs the cascade per pair, and the content evolves between pairs (the order is defined behavior)
- Success envelopes gain `pairs_total` + `pair_results`, and failures raise `EditPairFailed` with `failed_pair_index` (reuses `INVALID_INPUT`, exit 65)
- `--partial` (opt-in) applies the pairs that match, and zero applied pairs maps to `NO_MATCHES` (exit 1)

### Write target resolved first (v0.1.15, G118, ADR-0027)
- `cmd_write` resolves the target via `validate_path` once, before append/prepend, line-ending auto-detection and `--expect-checksum`
- It removes the double path identity (CWE-367): with a divergent CWD the append no longer truncates and a checksum drift exits 82
- Convention: every mutating command MUST resolve the target via `validate_path` before any `exists()`/read

### L1 WalPolicy + L4 HeuristicsEngine (v0.1.16, G119, ADR-0028, ADR-0062)
- `WalPolicy { Auto, Always, Never }` lets callers tune when the WAL sidecar is written, and `Auto` creates it for a target above 1 MiB, for `edit`/`replace`, or outside a git directory, and skips a trivial write (4 KiB or less) inside git
- `crate::wal::heuristics` aggregates the retention votes via `heuristics_should_preserve(target, committed_at_unix, count, rank)`, and the levers use constants + XDG/CLI configuration in v0.1.35+ (no product `ATOMWRITE_WAL_*` env knobs)
- The H3 rate limit is a creation-time vote only (`creation_throttled`): after 10 journals opened within 60 s `Auto` stops opening new ones while the writes stay atomic, and a journal that was written is always removed (ADR-0062)
- The `wal_policy` field on the `WriteOutput` NDJSON exposes the decision per call

### L3 startup auto-heal (v0.1.17, G119, ADR-0028, ADR-0062)
- `atomwrite` runs an autonomous `wal-heal` pass before dispatch (`startup_wal_heal` in `src/lib_dispatch.inc.rs`) with a 3600 s threshold and a 100 ms budget
- It runs only for a verb that mutates, and only over the directories named in argv, never the whole workspace
- It removes `Committed` and `Aborted` sidecars and never a `Started` one, which is the orphan that deserves attention after a crash
- Opt-out via the global flag `--no-auto-heal` (`src/cli_global.inc.rs`), and it logs structured info when reaping, debug when nothing to reap, warn on failure

### 4-layer empty-stdin guard (v0.1.16, G120, ADR-0029)
- L1 rejects 0 bytes from stdin by default in `read_stdin_content` with opt-out `--allow-empty-stdin`
- L2 rejects empty stdin in `handle_append_prepend`
- L3 emits a `tracing::info!` warning when `--append` + `--expect-checksum` + empty stdin combine ambiguously, opt-out via `--no-checksum-when-empty`
- L4 always emits `stdin_bytes_read: u64` on the `WriteOutput` NDJSON for late agent gating

### G118 universal resolve-first + G117 edge cases (v0.1.18, ADR-0030)
- 10 mutating commands pre-validate root paths via `validate_path` before constructing any walker or worker: `write`, `edit`, `copy`, `apply`, `move`, `rollback`, `set`, `del`, `case`, `replace`
- `replace /etc/passwd` aborts in microseconds with a single `WORKSPACE_JAIL` envelope instead of walking the entire filesystem
- 3 regression tests for G117 edge cases: Unicode exact-match (UTF-8 diacritics), CRLF line-ending preservation after replace, multi-pair where the same `--old` appears twice
- 1 integration test for G120 L3: the `--append + --expect-checksum + --allow-empty-stdin` cross-flag combination is covered end-to-end

### Internationalization
- Translations embedded at compile time via rust-i18n (`locales/en.toml`, `locales/pt-BR.toml`)
- Detection: `sys-locale` once at startup, and on Linux that crate reads `LANGUAGE`, `LC_ALL`, `LC_MESSAGES` and `LANG`, while `directories` reads `XDG_*` and `HOME` for the config paths
- The product code calls no `env::var` (a test guards it), and the dependency reads are listed in ADR-0057
- Normalize raw tags with `unic-langid` (underscore → hyphen, strip `.UTF-8`), and `C`/`POSIX` yield no language preference
- Negotiate against the available locales with `fluent-langneg` (`NegotiationStrategy::Lookup`)
- Typed `Language` enum (`En` | `PtBr`, `#[non_exhaustive]`) + immutable `OnceLock<LocaleState>`
- Precedence: CLI `--locale` → preference under `storage::config_dir()` (XDG → locale) → OS → `en` (no product env knobs)
- Override: global `--locale` (clap value_parser, field name `lang` kept for API stability), and there is no product `ATOMWRITE_LANG` env knob in v0.1.35+
- Diagnostics: `atomwrite locale` NDJSON report, `locale --set` / `locale --clear` for the XDG preference
- NDJSON codes and error Display messages stay English (agent machine contract)
- Error suggestions and selected human stderr warnings follow the resolved locale via `t!`
- Optional Cargo features `i18n-cjk` / `i18n-rtl` / `i18n-europe` / `i18n-full` are placeholders: the default binary ships only complete `en` + `pt-BR`
- `build.rs` declares `cargo:rerun-if-changed` for `locales/`, `locales/en.toml`, and `locales/pt-BR.toml`
- Key parity tests: `tests/locale_parity.rs`

### Macros policy (one local declarative macro, no proc-macro crate)
- Decision: the production code defines a single project-local macro, `invalid_input!` (`src/error/mod.rs`, `macro_rules!`), which builds an `AtomwriteError::InvalidInput` error from a format string
- The test harness has one more, `impl_hermetic!` in `tests/common/mod.rs`, and there is no local `proc-macro = true` crate
- Exhaust generics, traits, functions, and `const` asserts before introducing a macro
- Ecosystem macros only where syntax cannot be a function:
  - Derive: `clap`, `serde`, `thiserror`, `schemars` for CLI / NDJSON / errors
  - Attribute: `#[tracing::instrument]` on command entry points
  - Function-like (external): `rust_i18n::i18n!`, `schemars::schema_for!`, `tracing::{info,debug,…}!`
  - std built-ins: `env!` / `option_env!` (version), `matches!`, `vec!`, `format!`, `write!`/`writeln!`, `const _: () = assert!(…)`
- Prefer typed `Serialize` + `JsonSchema` structs over ad-hoc `serde_json::json!` for agent NDJSON contracts
- `serde_json::json!` appears only in JSON Schema assembly (`src/schema_dispatch.inc.rs`, `src/schema_terminal.inc.rs`) and in the dynamic key values of the `config` verb (`src/commands/config_cmd*.rs`), and child re-tagging uses `output::ndjson_insert_field`
- No `todo!` / `unimplemented!` / `dbg!` in production paths, and the source guards forbid `panic!` and `.unwrap()` there (see Test Architecture)
- No local `proc-macro = true` crate (it would force the `syn`/`quote` compile cost on a CLI that does not need a DSL)


## Error Strategy
- `AtomwriteError` enum with thiserror derives Display
- Each variant maps to a sysexits-compatible exit code
- Error classification: permanent, transient, conflict, precondition_failed
- Transient and conflict errors are marked retryable for agent retry loops
- All errors serialize to NDJSON on stdout with machine-readable fields
- The `suggestion` field in `ErrorJson` provides actionable recovery guidance for each error variant
- The `ErrorContext` struct (added in v0.1.4) carries `workspace_provided: bool` and `workspace: Option<PathBuf>` from the CLI parser to the error output
- `ErrorJson::from_error_with_context(err, &ErrorContext)` produces context-aware suggestions
- The `WorkspaceJail` suggestion adapts based on whether the user supplied `--workspace` (CLI/XDG only in v0.1.35+)
- Legacy `ErrorJson::from_error(err)` delegates to `from_error_with_context` with `ErrorContext::default()` (backward compatible)
- 37 error variants in total (`rg -c '#\[error' src/error/variants.inc.rs`), and the agent manifest lists 41 exit-code entries with 33 distinct numbers (`atomwrite agent-surface --format json | jaq '.exit_codes'`)
- v0.1.24 typed error audit: ALL user-facing `anyhow::bail!()` converted to `AtomwriteError` variants, and no error path returns a generic exit 1 without a JSON envelope
- A failed run ends with exactly one terminal event with `ok: false` (ADR-0061)


## Architectural Decision Records (ADRs)
- See `docs/decisions/README.md` for the full ADR index, which is the authority for this list
- ADRs 0019 to 0078 live in `docs/decisions/`, each with a pt-BR translation, all following the Michael Nygard format (Status, Context, Decision, Consequences, Alternatives, Trigger to revisit)
- 0019 — tree-sitter-language-pack over direct tree-sitter dependency
- 0020 — WAL sidecar file path and shape
- 0021 — v14 query/outline accept only kind names, not S-expressions
- 0022 — G72 tree-sitter replaces the bracket heuristic
- 0023 — G114 WAL recovery is consultative, not automatic
- 0024 — get/del TOML dotted-path uses manual Table descent
- 0025 — `--positions` is opt-in and limited to the `--query`/`--tree` modes
- 0026 — G117 multi-pair edit: fuzzy parity, per-pair reporting, opt-in `--partial` (v0.1.15)
- 0027 — G118 write resolves the target against the workspace before all pre-steps (v0.1.15)
- 0028 — G119 five-layer autonomous WAL cleanup: L1 WalPolicy, L2 JournalGuard, L3 startup auto-heal, L4 HeuristicsEngine, L5 wal-stats (v0.1.15-v0.1.17)
- 0029 — G120 empty-stdin guard and cross-validation of `--append`, `--expect-checksum` and empty stdin (v0.1.16)
- 0030 — v0.1.18: replace pre-validates root paths, G120 L3 test, G117 edge cases
- 0031 — search and replace resolve root paths against the workspace via a shared helper (G121)
- 0032 — G122: real S-expression matching in `query`
- 0033 — v0.1.19 exit code documentation drift consolidation
- 0034 — help-driven testing: anti-pattern and the v0.1.20 corrective process
- 0035 — six-layer defense-in-depth for the `write` subcommand (v0.1.20)
- 0036 — `--partial` in `edit`: single-pair stays NO_MATCHES, multi-pair applies the matched ones
- 0037 — `GlobalArgs.lang` renamed to `--locale`, freeing the `--lang` namespace (v0.1.20)
- 0038 — a fulfilled backup is deleted: delete after success is the default (v0.1.21)
- 0039 — edit-loop helper: N edits in one invocation (v0.1.22)
- 0040 — prune-backups subcommand: manual cleanup of legacy backups (v0.1.22)
- 0041 — `allow_hyphen_values` for 12 CLI text fields across 8 structs since v0.1.37, because the three positionals require `--` (v0.1.23)
- 0042 — backup by default for content-mutating commands (v0.1.23)
- 0043 — shrink guard with `--expect-checksum` (v0.1.23)
- 0044 — `--old-file`/`--new-file` for the edit command (v0.1.23)
- 0045 — actionable suggestions for clap parse errors (v0.1.24)
- 0046 — diff resolve-first retrofit (v0.1.24)
- 0047 — scope read-only mode fix and `find_all` migration (v0.1.24)
- 0048 — unified `BackupOpts` and a single `resolve_backup()` resolver, with precedence CLI flags > `.atomwrite.toml` `[defaults]` / XDG > built-in default (env knobs removed by v0.1.35)
- 0049 — live config plumbing: `load_config` called once in `lib.rs::run()`, `DefaultsSection` propagated to every mutating handler (v0.1.28)
- 0050 — stdin-tty guard: `main.rs` computes `stdin.is_terminal()` and propagates `stdin_is_tty` down to `cmd_edit`, so stdin-consuming modes fail fast with exit 65 instead of blocking (v0.1.28)
- 0051 — v0.1.29 gap close, agent surface and binary honesty
- 0052 — v0.1.30 residual agent contract
- 0053 — clap rules audit exceptions (v0.1.32)
- 0054 — v0.1.34 one-shot fuzzy contract: one-pass multi-apply, `--timeout-secs` default 120 / exit 124, cancel polled mid-fuzzy, resource caps
- 0055 — v0.1.36 agent-first contract (GAP-2026-019…033): jail on content-source paths + scratch staging, `ok: false` error envelope, `similarity: null`, global `--select`/`--filter`/`--limit` (see `docs/decisions/0055-v0-1-36-agent-contract-gaps.md`)
- 0056 — remove the compile-time `release_max_level_info` cap from `tracing`
- 0057 — dependency environment reads, parser download, MSRV 1.90 and YAML stack
- 0058 — the `config` verb and typed tuning keys
- 0059 — one retry module with full jitter and a global deadline
- 0060 — a byte budget for `search` events in flight
- 0061 — one terminal event per failed run (`publish_failure` and `VerdictPublished`)
- 0062 — WAL rate limit at journal creation and a bounded `startup_wal_heal`
- 0063 — SIGPIPE: a cooperative handler with exit 141 (conscious deviation)
- 0064 — automatic thread ceiling of 16 and a rayon pool built on demand
- 0065 — fuzzy content ceiling and cooperative cancel polls
- 0066 — `DoctorChecksFailed`: `doctor --strict` exits 1 with one terminal line
- 0067 — closed terminal schemas and a faithful `--print-schema`
- 0068 — hermetic tests and source guards
- 0069 — split `constants.rs` and the large modules into `.inc.rs` parts
- 0070 — `watch` becomes an opt-in feature
- 0071 — `extract-artifact`: bounded signal, never raw bytes
- 0072 — agent-native surface: stream shaping, `--print-schema` before clap, side-effect guards
- 0073 — one terminal cancel point and `deadline_exceeded`


## Test Architecture
- Integration suites: 112 `.rs` files in `tests/` (count them with `fd -e rs -d 1 . tests`), plus unit tests colocated in `src/` and doctests, and the per-test total is not frozen in this document (count `#[test]` with `rg -c '#\[test\]' tests src`)
- Unit tests are colocated with the code under `#[cfg(test)]` modules
- Integration tests live in `tests/` and use `assert_cmd` + `predicates` for shell-out tests
- Every test that runs the binary goes through `common::hermetic(...)` (`tests/common/mod.rs`): an empty `HOME` and `XDG_*`, a fixed `LANG`, and the real XDG cache so the AST parsers are not downloaded again (ADR-0068)
- Source guards fail the build: `tests/cli_v0137_hardcode_guard.rs` (duration literals, `.bak.` infix, sidecar names, time-unit numbers outside `constants*`, `env::var`, `Command::new`, no `.sh` file, no `.github` directory) and `tests/cli_v0137_let_underscore_guard.rs` (`let _ =`, `.unwrap()`, `.expect(`, `panic!`, `todo!`, `unimplemented!` in production code), both with empty allowance lists
- Documentation guards: `tests/docs_consistency.rs` and `tests/docs_exit_codes.rs`
- Property-based tests via `proptest` for checksum, backup and fuzzy
- Cross-compile gate via `tests/cross_compile_check.rs`
- Snapshot tests via `insta` for stable NDJSON output
- Two fuzz targets under `fuzz/fuzz_targets/` (`batch_parse`, `extract_json`)
- v0.1.36 agent contract: `tests/cli_v0136_gaps.rs`


## v0.1.35 residual contracts (architecture notes)
- Large write guard: default-deny when the existing target size exceeds XDG `[write].confirm_large_bytes`, which requires `--ack-overwrite` (one-shot), and `--require-large-ack` is independent (not a clap alias of `--confirm`)
- Delete surface: `--plan` / `--dry-run` plan-only, and `--confirm` and `--yes`/`-y` are rejected fail-closed
- Watch: always emits the terminal NDJSON `type:watch_summary`, idle default 500 ms, debounce from CLI or XDG `[watch].debounce_ms`
- Semantic-merge: conflict markers default ON, and `--no-conflict-markers` opts out
- SRP: secondary monoliths are split via `include!` (ADR-0069)
- Observability: `init_tracing` only, local diagnostics on stderr, no product telemetry
- Config: CLI + XDG / `.atomwrite.toml` only for product knobs, and `doctor` reads no environment variable (host detection uses file probes only)
- Contract language: `message` and `reason` of NDJSON errors and the clap help text are English by contract, and only `suggestion` is localized (en and pt-BR, with `suggestion_lang`)


## v0.1.36 agent contract (ADR-0055)
- Historical (v0.1.36 and early v0.1.37): 46 subcommands (`doctor`, `locale`, `commands`, `scratch` completed the 41 → 45 jump, `extract-artifact` followed in v0.1.37), and the final v0.1.37 SSOT is 47 with `config`
- Error envelope always `ok: false` (required), `match_count` present (0 on most match failures)
- Fuzzy honesty: normalized strategies → `similarity: null`, dual-gate `block_anchor`, soft floor via XDG `[fuzzy].soft_floor_delta` (default 0.05) on `context_aware`
- Content-source paths (`--old-file`/`--new-file`) remain under the workspace jail, and agents stage via `scratch init` → `.atomwrite/scratch/` (policy, not a vulnerability)
- Global shape flags (`--select`/`--fields`, `--filter`, `--limit`) reshape stdout at runtime, they are not NDJSON schema fields, and errors always-emit under `--filter`


## Maintenance
- This document and `ARCHITECTURE.pt-BR.md` are maintained together, so a change to one lands in the other in the same delivery
- Counts here (error variants, schemas, test files) come from the commands shown next to them, re-measure before changing a number