Skip to main content

Module formatters

Module formatters 

Source
Expand description

§2 module 29 formatters (COMPOSABLE-HARNESS-DESIGN.md line 479): “D10/oc§10 format-on-write” — reuses the EXACT crate::tools::WriteObserver seam P5-9 built for checkpoint (D-5: “write-path interception seam shared with checkpoint”), rather than a second interception point.

§C10 (design line 534) — the critical correctness rule

“Post-write formatting invalidates the model’s file memory; formatter must diff-back into the result (oc§10).” Concretely: once a formatter rewrites a file the model just wrote/edited, the model’s IN-CONTEXT belief about that file’s bytes is stale. FormatObserver::after_write (when [capabilities.formatters] diff_back = true, the default) returns a unified diff of exactly what the formatter changed, appended to the calling tool’s result — so the model’s next action is informed by the ACTUAL on-disk bytes, not its own pre-format draft. diff_back = false still runs the formatter (the file changes) but withholds the annotation — the C10-UNSAFE mode, legal but never the default.

§Wire model — stdin -> stdout filter

A configured formatter is invoked as command args... with the JUST-WRITTEN file’s bytes piped to its stdin; its stdout (bounded, MAX_FORMATTER_OUTPUT_BYTES) becomes the new file content IF the process exits 0 and produces non-empty output that differs from the input. This is the standard “formatter as filter” contract real tools already support in this mode (gofmt reads stdin/writes stdout by default; rustfmt --emit stdout; prettier --stdin-filepath <name>; black -), and it needs no %f-style path-templating in the config schema — the weakest form that still composes with arbitrary real formatters. A non-zero exit, a timeout, or empty output is treated as “formatter had nothing useful to say” and never corrupts the file: the on-disk content from the write/edit tool is left exactly as that tool produced it.

§Ordering (composes with checkpoint + lsp on the shared seam)

crate::agent::build_tool_context installs observers in the order checkpoint -> formatters -> lsp via crate::tools::WriteObserverChain: checkpoint’s before_write captures the pre-image before ANY mutation; this module’s after_write reformats the just-written file; lsp’s after_write (running AFTER this one in the same chain) then reads the FINAL, formatted file for diagnostics — never the model’s pre-format draft.

Structs§

FormatObserver
The crate::tools::WriteObserver [capabilities.formatters] installs. before_write is a true no-op (format-on-write only ever acts AFTER a mutation). after_write runs the configured formatter (if any matches path’s extension), rewrites the file when the formatter’s output differs from what was just written, and — when Self::diff_back is true — returns a unified diff annotation so the calling tool’s result stays truthful about the file’s final bytes (C10).
FormatterSpec
One [capabilities.formatters.<name>] entry — a user-configured formatter command, invoked as a stdin->stdout filter (see module doc comment). command/args are config-borne code execution (D-10) — stripped from an untrusted project layer exactly like [capabilities.lsp.servers.*]/hooks/mcp.servers.

Constants§

DEFAULT_FORMATTER_TIMEOUT_SECS
Default per-invocation timeout — a hanging formatter can’t hang the write-path loop (build brief: “timeout + kill like hooks”).
MAX_DIFF_CHARS
Bound on the unified diff text appended to a tool result — a formatter that rewrites a huge file can’t blow the model’s context with a huge diff either (same bounded-annotation posture as crate::lsp’s diagnostics cap).
MAX_FORMATTER_OUTPUT_BYTES
Bound on a single formatter invocation’s stdout — a hostile or broken configured formatter can’t force unbounded memory growth (same rationale as crate::mcp::MCP_MAX_RESPONSE_BYTES).

Functions§

observer_for_config
Build the FormatObserver a fresh crate::Agent should install, given a resolved crate::Config — called once, from crate::agent::build_tool_context. Config::formatters_enabled is the ONE gate: false (the default) returns None — no formatter ever runs, byte-identical to before this module existed.