- 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_tracinginsrc/runtime.rs, no product telemetry): non-blocking writer, filter from CLI-v/-q, optional XDG state log file (keylog.file), no productATOMWRITE_*/RUST_LOGknobs in v0.1.35+ - The agent data plane remains NDJSON on stdout
src/main.rs— binary entry: signal setup, tracing init, dispatchsrc/lib.rs— library root: module declarations and therun()dispatchersrc/lib_dispatch.inc.rs— command dispatch andstartup_wal_heal, the startupwal-healpass that runs before dispatch, only for verbs that mutate and only over the directories named in argvsrc/runtime.rs(+runtime_cli.inc.rs) — binary startup helpers: tracing, locale, clap error enrichment, schema prescansrc/cli.rs(+cli_global.inc.rs) — clap#[derive(Parser)]with the global flagssrc/cli_args/— per-subcommand argument structs and value enums, split inmod.rsandpart*.inc.rs
src/atomic/— atomic write pipeline:write.rsorchestrates tempfile + fsync + rename + fsync dir,rename_path.rsis the tempfile path,inplace.rsholds the in-place write, the EXDEV fallback and timestamped backups,persist_retry.rsretries the Windows tempfile persist,types.rsandtime_fmt.rsare helperssrc/checksum.rs— BLAKE3 hash computation for files and byte slices (uses memmap2 for large files)src/file_io.rs— smart file reading: one open, onefstaton the handle, one read into an exact-size buffer (try_reserve_exact), hashing useschecksum::hash_filesrc/platform.rs— platform durability and rename:Durabilityfull|fast|auto, Linuxrenameat2with rename fallback, macOSF_FULLFSYNCon full, and the write NDJSON reportsplatform.durabilityandplatform.rename_methodsrc/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_normalizedandcontext_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_countandindent_adjustedinEditOutput, normalized strategies emitsimilarity: null(not a fake 1.0),block_anchordual-gates the middle ratio, andcontext_awareapplies a soft floor via XDG[fuzzy].soft_floor_delta(default 0.05) - Reflink (copy-on-write) lives in
src/atomic/write.rsandsrc/atomic/inplace.rs, through thereflink-copycrate
src/path_safety.rs— workspace jail: path traversal prevention, symlink validation, FIFO/device detection, Windows reserved names (COM0–9/LPT0–9), NFC, long-path\\?\helpersrc/storage.rs— config/data/cache/state viadirectories::ProjectDirs(XDG), no productATOMWRITE_HOMEruntime knob in v0.1.35+src/env_detect.rs— WSL/container/K8s/Termux/Flatpak/Snap autodetection fordoctor, 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, andCancelCheckfor the cooperative cancelsrc/stdin_cancel.rs— cancellable stdin: a reader thread that gives up when the run is cancelled or the deadline passessrc/error/— domain error enum with exit codes, error classification and the retryable flag, split invariants.inc.rs,class.inc.rs,impl_methods.inc.rs,error_json.inc.rsandsuggest.inc.rssrc/lock.rs— advisory file locking viastd::fs::File::try_lock, which isflockon Unix andLockFileExon Windows, on the target's own inode; the.<target>.atomwrite.locksidecar is taken after it, best effort, so older releases stay mutually excluded;LockGuard::target_unchangedrevalidates the inode withsame_fileimmediately before the swap,deletetakes the same lock before it hashes and unlinks, and the sidecar name becomes a short BLAKE3 digest when the literal name would pass 255 bytes (ADR-0079)src/walk_globs.rs— the one place that compiles the--include/--excludeglobs of every verb, a glob that does not compile isINVALID_INPUT(exit 65)
src/wal/— the WAL sidecar, one module per concernsrc/wal/journal.rs— sidecar writer: appendsStartedandCommittedentries to.atomwrite.journal.<target>.atomwrite.journal.json, and theJournalGuardRAII from G119 L2src/wal/policy.rs—WalPolicy(Auto,Always,Never) and the creation heuristics, includingcreation_throttled, the H3 rate limit applied only when a journal is about to be createdsrc/wal/heuristics.rs— G119 L4 HeuristicsEngine:h1_ttl,h2_lru_within_cap,h3_rate_limit,h4_sentinel,h5_archive, whereheuristics_should_preserveaggregates the retention votes for a committed journal and does not consulth3_rate_limitsrc/wal/heal.rs—recover_orphan_journals(dir),auto_heal_scopedand thewal-healpass, which removesCommittedandAbortedsidecars, and removes aStartedsidecar only when the owning PID no longer exists, reported asdead_owner_removedsrc/wal/stats.rsandsrc/commands/wal_stats.rs— G119 L5wal-statssubcommand: total_journals, by_state, oldest_age, total_size, by_directory, auto_heal_recommended, estimated_reclaim_bytes
src/syntax_check.rs— REAL tree-sitter syntax check viatree-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 theastfeature is off
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 eventsrc/output_shape.inc.rsandsrc/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 fieldssrc/ndjson/— output type definitions with schemars JSON Schema derivationsrc/schema_dispatch.inc.rsandsrc/schema_terminal.inc.rs— the--print-schemalisting and the closed terminal schemassrc/error/— the error envelope always emitsok: false(required) plusmatch_counton match failures (0 on most failures), with an optionalbest_candidateon miss/ambiguitysrc/constants.rs— named constants for buffer sizes, thresholds and exit codes
src/binary_detect.rs— null-byte heuristic for binary content detectionsrc/line_endings.rs— LF/CRLF/CR detection and normalizationsrc/lang_utils.rs— programming-language extension maps for AST commandssrc/locale.rs(+locale_persist.inc.rs) — UI locale:Language, sys-locale detect, unic-langid parse, fluent-langneg negotiate, OnceLock, XDG preference filesrc/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,WalkParallelcollectionsrc/config.rs— loading of.atomwrite.tomland the XDGconfig.toml(flags > workspace > global > defaults)
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-secsdeadline and the shutdown flagsrc/best_effort.rs— the sanctioned way to ignore a failure on purpose:best_effortlogs atdebugand 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-widetuning()accessorsrc/commands/config_cmd.rs(+config_cmd_keys.inc.rs,config_cmd_write.inc.rs) — theconfigverb: one typed table of 43 keys, layered read (flag, workspace, global, built-in), validated atomic write that keeps commentssrc/constants.rs— includesconstants_concurrency.inc.rs(retry, thread ceiling, search budget),constants_config.inc.rs(config file names, key minimums) andconstants_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, keysearch.max_inflight_bytes), and the stream being drained never waits, so it cannot deadlocksrc/ndjson/part3.inc.rs—FileFailure,FailureSummary,FailureTallyandpartial_of:files_failed,partialanderrors[](at most 50) on the terminal summary of every verb that walks a tree, withpartialderived from the same countsrc/output_verdict.inc.rs— success/failure stamping of the terminal event and theVerdictPublishedmarker, so a failing summary is the only error line of the runworkspace_source— every terminal envelope publishesargvorcwd, so a run that took the root from the process working directory says so instead of hiding the ambient authorityreplace --expect-checksum— the comparison runs under the lock and a drift exits 82STATE_DRIFT, closing the window between the check and the writebatch --transaction— every target lock is taken in path order before the snapshot and held until commit or rollback, so two batches wait instead of interleavingedit-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.rsand the fsync helpers raiseAtomwriteError, so a denied directory exits 13PERMISSION_DENIEDand never 255INTERNAL_ERROR src/commands/replace_file.inc.rsandsrc/commands/replace_finish.inc.rs— the per-file step and the closing step ofreplace, split out ofreplace_run.inc.rssrc/walk_globs.rs,src/stdin_cancel.rsandsrc/signal_cancel.inc.rs— the shared glob compiler, the cancellable stdin and the cancel helpers listed above
src/commands/— 47 subcommand implementations (handlers + aliases, SSOTCANONICAL_SUBCOMMAND_COUNT/agent-surface), each in its own module where applicable, andbatch/,edit/,extract_artifact/andscope/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 initstaging under.atomwrite/scratch/), andextract-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
core— slim binary without AST/watch (PRD 5–8 MiB target, 26,560,352 bytes measured on 2026-11-04, DERIVADO 26560352 / 1048576 = 25.33 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/transformwatch— filesystem watch via notify (watchsubcommand), opt-in, and the default build answerswatchwith exit 78 and a rebuild hintfull— default + lang-full + watchsemantic-searchhas 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 itoutline,query,scope,transformandsparse outlineexit 78 with a rebuild hint (CONFIG_INVALID), read fromsrc/commands/*_stub.rsandsrc/commands/sparse.rs, the slim build was not executed for this document cargo xtask check-featuresrunscargo check --locked --no-default-featuresfor thecoreandcore,watchsets and stops at the first failure (xtask/src/main.rs)
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
- 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
- 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
ok: falsepluscodeandexit, and theerrordiscriminator of 0.1.37 and earlier is gone
- 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 toftruncate(0) + write_all + fsync_datato preserve the inode, and NDJSON gainswrite_strategy: "rename" | "inplace" | "copyback"
- 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)
- 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_pipeinsrc/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-threadis_shutdown()see the same flag - Cooperative cancel also covers global
--timeout-secs(default 120, pass0to disable, exit 124 on deadline, GNUtimeoutconvention) - Fuzzy cascade polls the cancel flag mid-file (strategies + sliding windows) so the timeout works during
replace --fuzzy/ edit / batch / edit-loop signal::cancelled_errorstamps 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/ systemdsd_notify(N/A)
- Replaces the v0.1.11 bracket-balance heuristic that had false positives (Python indentation, JS template literals) and false negatives (Python
importof missing module) - Uses
tree-sitter-language-packwithdownload+dynamic-loadingfeatures - 23 languages are mapped by extension, and unknown extensions fall back to the legacy heuristic
- Exit 88 with first error line/column/kind/message
- Sidecar path:
.atomwrite.journal.<target>.atomwrite.journal.json - Appends
Started(op_id, expected_new_checksum, pid, started_at_unix) andCommitted(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
- 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 ofgithub.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_64bundle, 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-sitteras a direct dependency, but it adds hundreds of parser crates to the compile time (ADR-0019)
set/get/delusetoml_edit(preserves comments and key order) for TOML andserde_json(canonical) for JSONget/deluse manualTabledescent via theget_toml_pathandremove_toml_pathhelpers (ADR-0024) instead oftoml_edit::Document::get, which treats dotted keys as literalcaseuses theheckcrate for 5 identifier-case stylesquery/outlineuse iterative DFS via aVec<Node>stack to avoid stack overflow on deep files (compared to a recursiveTreeCursortraversal)
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 raiseEditPairFailedwithfailed_pair_index(reusesINVALID_INPUT, exit 65) --partial(opt-in) applies the pairs that match, and zero applied pairs maps toNO_MATCHES(exit 1)
cmd_writeresolves the target viavalidate_pathonce, 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_pathbefore anyexists()/read
WalPolicy { Auto, Always, Never }lets callers tune when the WAL sidecar is written, andAutocreates it for a target above 1 MiB, foredit/replace, or outside a git directory, and skips a trivial write (4 KiB or less) inside gitcrate::wal::heuristicsaggregates the retention votes viaheuristics_should_preserve(target, committed_at_unix, count, rank), and the levers use constants + XDG/CLI configuration in v0.1.35+ (no productATOMWRITE_WAL_*env knobs)- The H3 rate limit is a creation-time vote only (
creation_throttled): after 10 journals opened within 60 sAutostops opening new ones while the writes stay atomic, and a journal that was written is always removed (ADR-0062) - The
wal_policyfield on theWriteOutputNDJSON exposes the decision per call
atomwriteruns an autonomouswal-healpass before dispatch (startup_wal_healinsrc/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
CommittedandAbortedsidecars and never aStartedone, 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
- L1 rejects 0 bytes from stdin by default in
read_stdin_contentwith 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: u64on theWriteOutputNDJSON for late agent gating
- 10 mutating commands pre-validate root paths via
validate_pathbefore constructing any walker or worker:write,edit,copy,apply,move,rollback,set,del,case,replace replace /etc/passwdaborts in microseconds with a singleWORKSPACE_JAILenvelope 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
--oldappears twice - 1 integration test for G120 L3: the
--append + --expect-checksum + --allow-empty-stdincross-flag combination is covered end-to-end
- Translations embedded at compile time via rust-i18n (
locales/en.toml,locales/pt-BR.toml) - Detection:
sys-localeonce at startup, and on Linux that crate readsLANGUAGE,LC_ALL,LC_MESSAGESandLANG, whiledirectoriesreadsXDG_*andHOMEfor 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), andC/POSIXyield no language preference - Negotiate against the available locales with
fluent-langneg(NegotiationStrategy::Lookup) - Typed
Languageenum (En|PtBr,#[non_exhaustive]) + immutableOnceLock<LocaleState> - Precedence: CLI
--locale→ preference understorage::config_dir()(XDG → locale) → OS →en(no product env knobs) - Override: global
--locale(clap value_parser, field namelangkept for API stability), and there is no productATOMWRITE_LANGenv knob in v0.1.35+ - Diagnostics:
atomwrite localeNDJSON report,locale --set/locale --clearfor 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-fullare placeholders: the default binary ships only completeen+pt-BR build.rsdeclarescargo:rerun-if-changedforlocales/,locales/en.toml, andlocales/pt-BR.toml- Key parity tests:
tests/locale_parity.rs
- Decision: the production code defines a single project-local macro,
invalid_input!(src/error/mod.rs,macro_rules!), which builds anAtomwriteError::InvalidInputerror from a format string - The test harness has one more,
impl_hermetic!intests/common/mod.rs, and there is no localproc-macro = truecrate - Exhaust generics, traits, functions, and
constasserts before introducing a macro - Ecosystem macros only where syntax cannot be a function:
- Derive:
clap,serde,thiserror,schemarsfor 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!(…)
- Derive:
- Prefer typed
Serialize+JsonSchemastructs over ad-hocserde_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 theconfigverb (src/commands/config_cmd*.rs), and child re-tagging usesoutput::ndjson_insert_field- No
todo!/unimplemented!/dbg!in production paths, and the source guards forbidpanic!and.unwrap()there (see Test Architecture) - No local
proc-macro = truecrate (it would force thesyn/quotecompile cost on a CLI that does not need a DSL)
AtomwriteErrorenum 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
suggestionfield inErrorJsonprovides actionable recovery guidance for each error variant - The
ErrorContextstruct (added in v0.1.4) carriesworkspace_provided: boolandworkspace: Option<PathBuf>from the CLI parser to the error output ErrorJson::from_error_with_context(err, &ErrorContext)produces context-aware suggestions- The
WorkspaceJailsuggestion adapts based on whether the user supplied--workspace(CLI/XDG only in v0.1.35+) - Legacy
ErrorJson::from_error(err)delegates tofrom_error_with_contextwithErrorContext::default()(backward compatible) - 38 error variants in total (
rg -c '#\[error' src/error/variants.inc.rs), and the agent manifest lists 42 exit-code entries with 34 distinct numbers (atomwrite agent-surface --format json | jaq '.exit_codes') - v0.1.24 typed error audit: ALL user-facing
anyhow::bail!()converted toAtomwriteErrorvariants, 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)
- See
docs/decisions/README.mdfor the full ADR index, which is the authority for this list - ADRs 0019 to 0095 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 —
--positionsis opt-in and limited to the--query/--treemodes - 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-checksumand 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
writesubcommand (v0.1.20) - 0036 —
--partialinedit: single-pair stays NO_MATCHES, multi-pair applies the matched ones - 0037 —
GlobalArgs.langrenamed to--locale, freeing the--langnamespace (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_valuesfor 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-filefor 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_allmigration (v0.1.24) - 0048 — unified
BackupOptsand a singleresolve_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_configcalled once inlib.rs::run(),DefaultsSectionpropagated to every mutating handler (v0.1.28) - 0050 — stdin-tty guard:
main.rscomputesstdin.is_terminal()and propagatesstdin_is_ttydown tocmd_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-secsdefault 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: falseerror envelope,similarity: null, global--select/--filter/--limit(seedocs/decisions/0055-v0-1-36-agent-contract-gaps.md) - 0056 — remove the compile-time
release_max_level_infocap fromtracing - 0057 — dependency environment reads, parser download, MSRV 1.90 and YAML stack
- 0058 — the
configverb and typed tuning keys - 0059 — one retry module with full jitter and a global deadline
- 0060 — a byte budget for
searchevents in flight - 0061 — one terminal event per failed run (
publish_failureandVerdictPublished) - 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 --strictexits 1 with one terminal line - 0067 — closed terminal schemas and a faithful
--print-schema - 0068 — hermetic tests and source guards
- 0069 — split
constants.rsand the large modules into.inc.rsparts - 0070 —
watchbecomes an opt-in feature - 0071 —
extract-artifact: bounded signal, never raw bytes - 0072 — agent-native surface: stream shaping,
--print-schemabefore clap, side-effect guards - 0073 — one terminal cancel point and
deadline_exceeded - 0074 — strict apply runs every read-modify-write under one lock
- 0075 — grammar pack 1.22, ast-grep 0.51 and MSRV 1.90
- 0076 —
readaccepts several files in the raw format - 0077 — answer family and walk honesty
- 0078 — one schema per verb, served as a union
- 0079 — the lock is taken on the target inode
- 0080 —
sparse outlinecounts the journal under--hidden - 0081 — the verdict fields name the mechanism and the subject
- 0082 — the sparse family walks one policy
- 0083 —
--hiddenfollows the flag in every walking verb - 0084 — every mutating verb publishes the write verdict
- 0085 — the edit family publishes the write verdict
- 0086 —
applypublishes the backup path and the rollback - 0087 — the transaction snapshot follows the backup policy
- 0088 — the deletion keeps its undo and the reversal declares what it kept
- 0089 — the target is always an array and the batch names its copies
- 0090 — the transaction owns the copy and a failed write names what it left
- 0091 — the transaction lends its copy to a rewrite
- 0092 — the delete copies only when asked
- 0093 — the backup key distinguishes unset from false
- 0094 — the recursive delete owns the whole named tree
- 0095 — search submatch offsets are bytes
- Integration suites: 112
.rsfiles intests/(count them withfd -e rs -d 1 . tests), plus unit tests colocated insrc/and doctests, and the per-test total is not frozen in this document (count#[test]withrg -c '#\[test\]' tests src) - Unit tests are colocated with the code under
#[cfg(test)]modules - Integration tests live in
tests/and useassert_cmd+predicatesfor shell-out tests - Every test that runs the binary goes through
common::hermetic(...)(tests/common/mod.rs): an emptyHOMEandXDG_*, a fixedLANG, 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 outsideconstants*,env::var,Command::new, no.shfile, no.githubdirectory) andtests/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.rsandtests/docs_exit_codes.rs - Property-based tests via
proptestfor checksum, backup and fuzzy - Cross-compile gate via
tests/cross_compile_check.rs - Snapshot tests via
instafor stable NDJSON output - Two fuzz targets under
fuzz/fuzz_targets/(batch_parse,extract_json) - v0.1.36 agent contract:
tests/cli_v0136_gaps.rs
- 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-ackis independent (not a clap alias of--confirm) - Delete surface:
--plan/--dry-runplan-only, and--confirmand--yes/-yare 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-markersopts out - SRP: secondary monoliths are split via
include!(ADR-0069) - Observability:
init_tracingonly, local diagnostics on stderr, no product telemetry - Config: CLI + XDG /
.atomwrite.tomlonly for product knobs, anddoctorreads no environment variable (host detection uses file probes only) - Contract language:
messageandreasonof NDJSON errors and the clap help text are English by contract, and onlysuggestionis localized (en and pt-BR, withsuggestion_lang)
- Historical (v0.1.36 and early v0.1.37): 46 subcommands (
doctor,locale,commands,scratchcompleted the 41 → 45 jump,extract-artifactfollowed in v0.1.37), and the final v0.1.37 SSOT is 47 withconfig - Error envelope always
ok: false(required),match_countpresent (0 on most match failures) - Fuzzy honesty: normalized strategies →
similarity: null, dual-gateblock_anchor, soft floor via XDG[fuzzy].soft_floor_delta(default 0.05) oncontext_aware - Content-source paths (
--old-file/--new-file) remain under the workspace jail, and agents stage viascratch 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
readcomposes the cap (read.max_output_bytes, 24,000 bytes), the range windows,--number,--symbol,--line N --enclosingand--no-commentsin one pass over the same line visitor- The raw path streams a single target through the same
stream_raw_targetsthe multi-target path already used, so the peak memory no longer grows with the file size, and BLAKE3 runs only under--verify-checksum - The cap cuts on a complete line and at the end of the last outline item, the NDJSON event carries
truncated: truewith thenexthint, and the stderr notice carries the exact--linesthat reads the rest --no-commentswithout--numberis refused, because a dropped line would point the follow-upapplyat the wrong one
search --format text,list --format textandoutline --format tsvanswer in the shape the native agent tools already parsesearch.format,read.numberandoutput.profileare XDG keys, so the cheap shape does not depend on a flag repeated on every calloutlineparses raw bytes, publishesparse_errorswithfirst_error_line, and closes a Markdown section at the line before the next heading of the same or higher levelsearchcloses withALL_SKIPPED_BINARY(exit 89) when every candidate was binary, so "nothing was searched" stops looking like "nothing matched"
applyreads every file of a multi-file patch, matches whole lines, and refuses an ambiguous or malformed block before the first write- A write that fails after another file was written restores the files already written, so the call leaves no half-applied patch
- The syntax check compares the parse-error count before and after the swap and refuses only on a rise, so a file that already had an error keeps being editable (ADR-0086)
apply --showanddiffpublish 1-based lines, anddiffseparatesold_linefromnew_line
- The lock is
std::fs::File::try_lockon the target inode, and the.<target>.atomwrite.locksidecar is a compatibility file whose failure no longer aborts the write (ADR-0079) - The failure envelope carries
ok,code,exit,pathandsuggestion;message,error_class,retryableandsuggestion_langappear only under-v, andtarget_resolvedis always an array (ADR-0089) - Every mutating verb publishes
write_strategyand, when a copy was refused,backup_skipped(ADR-0084, ADR-0085) delete --recursiveowns the whole tree in one call and publishesbackups_kept,backup_bytesanddirs_leftwhen a backup or a leftover exists, so the caller learns what remains without walking again
- This document and
ARCHITECTURE.pt-BR.mdare 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