@ian-pascoe/pi-formatter
v0.5.1
Published
Configured automatic post-edit formatting for Pi
Maintainers
Readme
@ian-pascoe/pi-formatter
Configured automatic post-edit formatting for Pi.
Install
pi install npm:@ian-pascoe/pi-formatter
# or
pi install git:github.com/ian-pascoe/pi-extensionsFor a local checkout, run pi -e ./packages/pi-formatter/src/index.ts.
Install formatter executables separately.
Settings
Pi Formatter reads the formatter key from Pi's global ~/.pi/agent/settings.json and trusted
project .pi/settings.json:
{
"formatter": {
"timeoutMs": 30000,
"formatters": {
"markdownlint-cli2": {
"command": "markdownlint-cli2",
"args": ["--fix", "$FILE"],
"files": {
"extensions": [".md", ".mdx"],
"fileNames": ["README"]
},
"requireRootMarker": true,
"rootMarkers": ["package.json", ".git"],
"environment": {}
}
}
}
}Each formatter requires a non-empty command and at least one file extension or exact basename.
Extensions include the leading period. Matching is case-sensitive. Arguments are passed directly
to the executable without a shell. Every $FILE substring in an argument is replaced with the
absolute changed-file path.
A formatter using $FILE runs once per matching changed file. A formatter without $FILE runs
once per matching workspace root, allowing full-project formatting. rootMarkers are basename glob
patterns; the nearest matching ancestor becomes the command working directory and Pi's working
directory is the fallback. Set requireRootMarker to true to skip the formatter unless any root
marker exists above the changed file; it defaults to false. A required empty rootMarkers list is
invalid. Formatters run sequentially in configuration order.
When formatters change a file, a Formatted by line per changed file is appended to the original
tool result so the agent knows its view of the file is stale, for example
Formatted by ruff-fix, ruff-format: lines 3–13 changed. It names every File Formatter and
Workspace Formatter that changed the file, in run order. Line numbers describe the formatted
file: the span runs from the first line that differs from the content before any formatter ran to
the last one, and it includes unchanged lines between them. When formatting only removed lines,
the line says lines removed after line N, lines removed before line 1, or all lines removed.
It starts with the file path, relative to Pi's working directory, when the mutation changed more
than one file. Nothing is added when the final content equals the original. A formatter that exits
non-zero or times out after writing changes is reported too; the Formatted by lines follow
any warnings.
Each Formatted by line is followed by a compact unified diff of what the formatters changed,
measured against the content before any formatter ran: - lines are the original text, + lines
are what is on disk now, up to three unchanged lines ( ) surround each change, and
\ No newline at end of file marks a line with no final newline. To edit the formatted file
without reading it again, take the and + lines, drop their first character, and use them
as oldText. The diffs of all files in one mutation result share a budget of 60 lines and
6,000 bytes, not counting the Formatted by lines. A file whose diff does not fit in what
remains, or whose formatter changed more than 100 lines, keeps only its Formatted by line, so a
large reformat stays bounded and the agent re-reads the file.
A timeout, spawn error, or non-zero exit appends a warning to the original tool result without
changing that result's success state; later formatters still run. The warning ends with a pointer
to the package's troubleshooting Skill, except when the formatter's stderr reports a syntax error
in the changed file: it matches wording such as SyntaxError, parse error, or Unexpected
token, names the file, and does not mention configuration outside file paths. That is an input outcome that Pi
LSP's Post-edit Diagnostics report when Pi LSP is installed, not a formatter failure to diagnose.
Bad-configuration errors and Workspace Formatter failures keep the pointer.
Declaring a syntax-error signal
That wording heuristic is a guess for formatters other than oxfmt, ruff, and stylua. A File
Formatter can instead declare how it reports a syntax error with syntaxErrorPattern, a
JavaScript regular expression source string tested against the formatter's stderr. The pattern is
case-sensitive and has no flags, so ^ and $ anchor to the whole trimmed stderr, not to each
line; match a line start with (?:^|\n). JSON strings need doubled backslashes.
{
"formatter": {
"formatters": {
"oxfmt": {
"command": "oxfmt",
"args": ["$FILE"],
"files": { "extensions": [".ts", ".tsx", ".js", ".json"] },
"syntaxErrorPattern": "(?:^|\\n) *x "
},
"ruff-format": {
"command": "ruff",
"args": ["format", "$FILE"],
"files": { "extensions": [".py", ".pyi"] },
"syntaxErrorPattern": "(?:^|\\n)error: Failed to parse "
}
}
}
}oxfmt prints a syntax error as a x <message> diagnostic and reports a bad configuration file as
Failed to load configuration file.. ruff prints a syntax error as error: Failed to parse
<file>:<line>:<column>: …, possibly after warning: lines, while a bad configuration file appears
on a later Cause: Failed to parse … line. Forced colour (FORCE_COLOR) changes stderr: ruff adds
ANSI codes and oxfmt prints × instead of x, so these patterns then miss and the pointer stays.
When syntaxErrorPattern is set, it replaces the heuristic for that formatter: a non-zero exit
whose stderr matches it is an input outcome without the pointer, and any other non-zero exit keeps
the pointer. The stderr need not name the file, and it is not checked for configuration wording, so make
the pattern specific enough not to match the formatter's configuration errors. Timeouts and spawn errors
always keep the pointer. Without the setting, the heuristic applies unchanged. The setting needs
$FILE in args, and an empty or invalid regular expression quarantines the definition with a
startup warning. The pattern is never shown to the model.
Global and project timeoutMs values override by scope. A project formatter replaces the complete
global definition with the same ID; set it to null to disable it. Invalid definitions and fields
are quarantined individually and reported at session startup. An invalid project replacement still
shadows the global formatter. Untrusted project settings are ignored.
Supported mutations
Formatting runs after successful native edit and write operations, Codex-style apply_patch
results, and applied Pi LSP Workspace Edit Previews. Changed and created files plus rename
destinations are formatted; deleted or vanished files are skipped.
Formatting holds Pi's file mutation queue for the mutation's files, the same queue native edit
and write use. When a batch of parallel tool calls edits one file, each edit lands before or
after formatting, never during it: the first formatting covers every edit that landed before it,
and its diff shows only the formatters' changes. Later results of the batch usually find nothing
left to format, so only one of them carries the Formatted by line. An edit that arrives while a
formatter runs waits for it instead of being overwritten, and a file that a queued mutation
deleted or renamed meanwhile is skipped. A formatter that times out or is aborted is killed, and
the queue is held until its process tree closes its stderr, for at most two more seconds. Pi's
read does not join the queue, so a parallel read can still see the file before it is
formatted.
When the Git collection is installed, Pi Formatter loads before Pi LSP so Post-edit Diagnostics observe formatted content. Separately installed extensions depend on Pi's configured extension order.
Security
Trusted formatter settings execute arbitrary local programs with the Pi process's environment and permissions. Review project settings and formatter binaries before trusting a project.
