@gotgenes/pi-autoformat 0.1.0 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -3
- package/.github/workflows/release-please.yml +29 -0
- package/.markdownlint-cli2.yaml +14 -2
- package/.pi/extensions/pi-autoformat/config.json +3 -6
- package/.pi/prompts/README.md +59 -0
- package/.pi/prompts/plan-issue.md +64 -0
- package/.pi/prompts/retro.md +144 -0
- package/.pi/prompts/ship-issue.md +77 -0
- package/.pi/prompts/tdd-plan.md +67 -0
- package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +39 -0
- package/CHANGELOG.md +365 -0
- package/README.md +42 -109
- package/biome.json +1 -1
- package/docs/assets/logo.png +0 -0
- package/docs/assets/logo.svg +533 -0
- package/docs/configuration.md +358 -38
- package/docs/plans/0001-initial-implementation-plan.md +17 -9
- package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
- package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
- package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
- package/docs/plans/0010-acceptance-test-coverage.md +240 -0
- package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
- package/docs/plans/0013-fallback-chain-step-type.md +280 -0
- package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
- package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
- package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
- package/docs/plans/0022-pi-coding-agent-types.md +201 -0
- package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
- package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
- package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
- package/docs/retro/0013-fallback-chain-step-type.md +67 -0
- package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
- package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
- package/docs/retro/0022-pi-coding-agent-types.md +62 -0
- package/docs/testing.md +95 -0
- package/package.json +30 -11
- package/prek.toml +2 -2
- package/schemas/pi-autoformat.schema.json +145 -21
- package/src/builtin-formatters.ts +205 -0
- package/src/command-probe.ts +66 -0
- package/src/config-loader.ts +829 -90
- package/src/custom-mutation-tools.ts +125 -0
- package/src/extension.ts +469 -82
- package/src/format-scope.ts +118 -0
- package/src/formatter-config.ts +73 -36
- package/src/formatter-executor.ts +230 -34
- package/src/formatter-output-report.ts +149 -0
- package/src/formatter-registry.ts +139 -30
- package/src/index.ts +26 -5
- package/src/prompt-autoformatter.ts +148 -23
- package/src/shell-mutation-detector.ts +572 -0
- package/src/touched-files-queue.ts +72 -11
- package/test/acceptance-event-bus.test.ts +138 -0
- package/test/acceptance.test.ts +69 -0
- package/test/builtin-formatters.test.ts +382 -0
- package/test/command-probe.test.ts +79 -0
- package/test/config-loader.test.ts +640 -21
- package/test/custom-mutation-tools.test.ts +190 -0
- package/test/extension.test.ts +1535 -158
- package/test/fallback-acceptance.test.ts +98 -0
- package/test/fixtures/event-bus-emitter.ts +26 -0
- package/test/fixtures/formatter-recorder.mjs +25 -0
- package/test/format-scope.test.ts +139 -0
- package/test/formatter-config.test.ts +56 -5
- package/test/formatter-executor.test.ts +555 -35
- package/test/formatter-output-report.test.ts +178 -0
- package/test/formatter-registry.test.ts +330 -37
- package/test/helpers/rpc.ts +146 -0
- package/test/prompt-autoformatter.test.ts +315 -22
- package/test/schema.test.ts +149 -0
- package/test/shell-mutation-detector.test.ts +221 -0
- package/test/touched-files-queue.test.ts +40 -1
- package/test/types/theme-stub.test-d.ts +42 -0
package/docs/configuration.md
CHANGED
|
@@ -27,23 +27,16 @@ Latest:
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
29
29
|
"$schema": "https://raw.githubusercontent.com/gotgenes/pi-autoformat/main/schemas/pi-autoformat.schema.json",
|
|
30
|
-
"formatMode": "prompt",
|
|
31
30
|
"commandTimeoutMs": 10000,
|
|
32
|
-
"hideSummariesInTui": false,
|
|
33
31
|
"formatters": {
|
|
34
|
-
"
|
|
35
|
-
"command": ["
|
|
36
|
-
"extensions": [".js", ".ts", ".tsx", ".json", ".md"]
|
|
37
|
-
},
|
|
38
|
-
"markdownlint-cli2": {
|
|
39
|
-
"command": ["markdownlint-cli2", "--fix", "$FILE"],
|
|
40
|
-
"extensions": [".md"]
|
|
32
|
+
"biome": {
|
|
33
|
+
"command": ["biome", "check", "--write", "--files-ignore-unknown=true"]
|
|
41
34
|
}
|
|
42
35
|
},
|
|
43
36
|
"chains": {
|
|
44
|
-
".
|
|
45
|
-
".
|
|
46
|
-
".
|
|
37
|
+
".ts": ["biome"],
|
|
38
|
+
".tsx": ["biome"],
|
|
39
|
+
".json": ["biome"]
|
|
47
40
|
}
|
|
48
41
|
}
|
|
49
42
|
```
|
|
@@ -52,37 +45,219 @@ Pinned tag:
|
|
|
52
45
|
|
|
53
46
|
```json
|
|
54
47
|
{
|
|
55
|
-
"$schema": "https://raw.githubusercontent.com/gotgenes/pi-autoformat/
|
|
48
|
+
"$schema": "https://raw.githubusercontent.com/gotgenes/pi-autoformat/v2.4.1/schemas/pi-autoformat.schema.json"
|
|
56
49
|
}
|
|
57
50
|
```
|
|
58
51
|
|
|
59
52
|
## Settings reference
|
|
60
53
|
|
|
61
|
-
### `
|
|
54
|
+
### `commandTimeoutMs`
|
|
55
|
+
|
|
56
|
+
Timeout in milliseconds for each formatter command.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"commandTimeoutMs": 10000
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `formatScope`
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
Boundary used to filter the touched-files queue. Paths outside the configured
|
|
69
|
+
scope are dropped silently.
|
|
64
70
|
|
|
65
71
|
Allowed values:
|
|
66
72
|
|
|
67
|
-
- `"
|
|
68
|
-
- `
|
|
69
|
-
|
|
73
|
+
- `"repoRoot"` (default) — detect the Git toplevel via
|
|
74
|
+
`git rev-parse --show-toplevel` and use it as the scope. Falls back to `cwd`
|
|
75
|
+
when not in a Git repo.
|
|
76
|
+
- `"cwd"` — strict cwd subtree.
|
|
77
|
+
- `string[]` — explicit allowlist of roots, each resolved relative to `cwd`.
|
|
78
|
+
A path is in scope if it falls under any configured root.
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
Symlinks are resolved on both sides via `fs.realpath`, so a symlinked workspace
|
|
81
|
+
dep that resolves outside the scope is correctly filtered, and a symlink
|
|
82
|
+
pointing into the scope is correctly included.
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
Example:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"formatScope": ["packages/server", "packages/shared"]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `shellMutationDetection`
|
|
93
|
+
|
|
94
|
+
Opt-in detection of files mutated by shell (`bash`) commands. Disabled by
|
|
95
|
+
default; enable to surface files touched by `sed -i`, `mv`, `cp`, `touch`,
|
|
96
|
+
`tee`, redirections, or user-declared codegen wrappers.
|
|
97
|
+
|
|
98
|
+
Defaults:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"shellMutationDetection": {
|
|
103
|
+
"enabled": false,
|
|
104
|
+
"argumentParsing": true,
|
|
105
|
+
"snapshotGlobs": [],
|
|
106
|
+
"wrappers": []
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Fields:
|
|
112
|
+
|
|
113
|
+
- `enabled` — master switch. Defaults to `false`.
|
|
114
|
+
- `argumentParsing` — parse a small whitelist of known mutating commands
|
|
115
|
+
(`sed -i`, `mv`, `cp`, `touch`, `tee`, plus simple `>` / `>>`
|
|
116
|
+
redirections). Bails on pipelines, command substitutions, sequencing, and
|
|
117
|
+
unknown flags so the surface stays auditable.
|
|
118
|
+
- `snapshotGlobs` — globs whose mtimes are sampled before and after each
|
|
119
|
+
`bash` invocation. Files whose mtime advanced are treated as touched.
|
|
120
|
+
Capped at 5,000 entries with a warning on overflow. Defaults to `[]`.
|
|
121
|
+
- `wrappers` — shell command prefixes that already print the files they
|
|
122
|
+
touched on stdout. Each entry has a `prefix` (matched at the start of the
|
|
123
|
+
bash command) and optional `outputFormat` (currently only `"lines"`).
|
|
74
124
|
|
|
75
125
|
Example:
|
|
76
126
|
|
|
77
127
|
```json
|
|
78
128
|
{
|
|
79
|
-
"
|
|
129
|
+
"shellMutationDetection": {
|
|
130
|
+
"enabled": true,
|
|
131
|
+
"snapshotGlobs": ["src/**/*.ts", "docs/**/*.md"],
|
|
132
|
+
"wrappers": [{ "prefix": "pnpm codegen", "outputFormat": "lines" }]
|
|
133
|
+
}
|
|
80
134
|
}
|
|
81
135
|
```
|
|
82
136
|
|
|
137
|
+
Merge semantics: `snapshotGlobs` and `wrappers` arrays replace lower-precedence
|
|
138
|
+
values rather than merging — consistent with other array fields in this config.
|
|
139
|
+
|
|
140
|
+
### `customMutationTools`
|
|
141
|
+
|
|
142
|
+
Declare additional tool names whose results should be treated as file
|
|
143
|
+
mutations and routed into the touched-files queue. Useful for project- or
|
|
144
|
+
extension-specific tools that the agent calls directly.
|
|
145
|
+
|
|
146
|
+
Each entry must specify the tool name and exactly one of `pathField` or
|
|
147
|
+
`pathFields`, each a dotted path into the tool's `input` payload. A field
|
|
148
|
+
may resolve to a string or a string array; arrays are flattened.
|
|
149
|
+
|
|
150
|
+
Defaults to `[]`.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"customMutationTools": [
|
|
157
|
+
{ "toolName": "my-codegen", "pathField": "output" },
|
|
158
|
+
{ "toolName": "refactor", "pathFields": ["src", "dest"] }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Paths are normalized and scope-filtered by the same pipeline used for
|
|
164
|
+
`write`/`edit`, so you do not need to restate scope rules per tool.
|
|
165
|
+
|
|
166
|
+
### `eventBusMutationChannel`
|
|
167
|
+
|
|
168
|
+
Lets peer extensions publish touched files onto Pi's shared event bus and
|
|
169
|
+
have them flow through the same prompt-end formatter pipeline.
|
|
170
|
+
|
|
171
|
+
Defaults:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"eventBusMutationChannel": {
|
|
176
|
+
"enabled": true,
|
|
177
|
+
"channel": "autoformat:touched"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Fields:
|
|
183
|
+
|
|
184
|
+
- `enabled` — subscribe to the channel when Pi exposes `pi.events`.
|
|
185
|
+
Defaults to `true`.
|
|
186
|
+
- `channel` — channel name to subscribe to. Defaults to `"autoformat:touched"`.
|
|
187
|
+
|
|
188
|
+
Payload shape (best-effort; malformed payloads are silently ignored):
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
{ path: string } // single file
|
|
192
|
+
{ paths: string[] } // multiple files
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Paths are resolved relative to the session `cwd` and pass through the same
|
|
196
|
+
scope filter as every other mutation source.
|
|
197
|
+
|
|
198
|
+
### `formatterOutput`
|
|
199
|
+
|
|
200
|
+
Optional surfacing of formatter `stdout` / `stderr` in failure reports.
|
|
201
|
+
Defaults preserve concise reporting: nothing extra is printed and the option is fully opt-in.
|
|
202
|
+
|
|
203
|
+
Successful runs are **never** annotated with output, even when this option is enabled — the goal is debugging failures, not chatter on the happy path.
|
|
204
|
+
|
|
205
|
+
Fields:
|
|
206
|
+
|
|
207
|
+
- `onFailure` (`"none" | "stderr" | "both"`, default `"none"`) — which streams of a failed run to include beneath each failure line.
|
|
208
|
+
`"stderr"` is sufficient for most formatters (parser errors, lint diagnostics).
|
|
209
|
+
`"both"` also includes `stdout`, useful for tools that report on `stdout` (some compilers / type-checkers).
|
|
210
|
+
- `maxBytes` (integer, default `4096`) — hard byte cap per stream per failed run, applied to UTF-8 byte length.
|
|
211
|
+
When the cap is exceeded, the **tail** of the output is preserved (which is where stack traces and parser errors usually sit) and a `... (truncated, N earlier bytes)` marker is prefixed.
|
|
212
|
+
- `maxLines` (integer, default `40`) — hard line cap per stream per failed run, applied after byte trimming.
|
|
213
|
+
When exceeded, a `... (truncated, N earlier lines)` marker is prefixed.
|
|
214
|
+
|
|
215
|
+
Example (enable `stderr` surfacing with the defaults):
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"formatterOutput": {
|
|
220
|
+
"onFailure": "stderr"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Example (more aggressive caps for terse environments):
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"formatterOutput": {
|
|
230
|
+
"onFailure": "both",
|
|
231
|
+
"maxBytes": 1024,
|
|
232
|
+
"maxLines": 20
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The rendered failure block looks like:
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
Formatter failures in 1 batch:
|
|
241
|
+
prettier (exit 2): src/foo.ts
|
|
242
|
+
stderr:
|
|
243
|
+
src/foo.ts: SyntaxError: Unexpected token (3:11)
|
|
244
|
+
> 3 | export const = 3
|
|
245
|
+
| ^
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Identical content is used in both the interactive TUI (via `notify`) and non-interactive log output (via `console.warn`).
|
|
249
|
+
|
|
83
250
|
### `hideSummariesInTui`
|
|
84
251
|
|
|
85
|
-
Whether formatter summaries should be hidden in the interactive TUI.
|
|
252
|
+
Whether formatter **success** summaries should be hidden in the interactive TUI.
|
|
253
|
+
|
|
254
|
+
When the extension is loaded in a Pi UI, each prompt-end flush updates a persistent footer status line (`setStatus("autoformat", ...)`).
|
|
255
|
+
A happy-path flush renders a one-line success indicator like `✓ autoformat: 3 files (biome, prettier)`.
|
|
256
|
+
Failures additionally fire a `notify(..., "warning")` toast and leave an error-styled status (`✗ autoformat: 1 batch failed (prettier) — 2 ok`) so the user can revisit them later in the session.
|
|
257
|
+
|
|
258
|
+
Set `hideSummariesInTui` to `true` to suppress the success status line.
|
|
259
|
+
Failures still surface via both the warning notification and an error-styled footer status regardless of this setting.
|
|
260
|
+
In non-interactive contexts (no UI), this setting has no effect — summaries go to `console.log` / `console.warn` as before.
|
|
86
261
|
|
|
87
262
|
Example:
|
|
88
263
|
|
|
@@ -99,13 +274,23 @@ Formatter registry keyed by formatter name.
|
|
|
99
274
|
Each formatter can define:
|
|
100
275
|
|
|
101
276
|
- `command: string[]`
|
|
102
|
-
- `extensions: string[]`
|
|
103
277
|
- `environment?: Record<string, string>`
|
|
104
278
|
- `disabled?: boolean`
|
|
105
279
|
|
|
106
|
-
|
|
280
|
+
> **Deprecated:** earlier versions accepted an `extensions: string[]`
|
|
281
|
+
> field on each formatter. It was never read by dispatch and has been
|
|
282
|
+
> removed. The loader still accepts on-disk configs that carry it but
|
|
283
|
+
> emits a single deprecation notice and ignores the value — remove
|
|
284
|
+
> `extensions` from your formatter entries and rely on `chains` to
|
|
285
|
+
> declare which extensions a formatter runs against.
|
|
107
286
|
|
|
108
|
-
|
|
287
|
+
**Batch dispatch.** Touched file paths are appended to `command` as
|
|
288
|
+
trailing arguments. The executor runs each formatter once per chain
|
|
289
|
+
group, passing every file in the group as a single invocation. Do not
|
|
290
|
+
include file paths or the legacy `$FILE` token in `command` — it is
|
|
291
|
+
rejected at config-load time.
|
|
292
|
+
|
|
293
|
+
Formatter command resolution stays intentionally simple:
|
|
109
294
|
|
|
110
295
|
- commands run from the project `cwd`
|
|
111
296
|
- commands inherit the extension process environment and `PATH`
|
|
@@ -118,12 +303,10 @@ Example:
|
|
|
118
303
|
{
|
|
119
304
|
"formatters": {
|
|
120
305
|
"prettier": {
|
|
121
|
-
"command": ["pnpm", "exec", "prettier", "--write"
|
|
122
|
-
"extensions": [".js", ".ts", ".tsx", ".json", ".md"]
|
|
306
|
+
"command": ["pnpm", "exec", "prettier", "--write"]
|
|
123
307
|
},
|
|
124
308
|
"markdownlint-cli2": {
|
|
125
|
-
"command": ["pnpm", "exec", "markdownlint-cli2", "--fix"
|
|
126
|
-
"extensions": [".md"],
|
|
309
|
+
"command": ["pnpm", "exec", "markdownlint-cli2", "--fix"],
|
|
127
310
|
"environment": {
|
|
128
311
|
"CI": "1"
|
|
129
312
|
}
|
|
@@ -136,39 +319,176 @@ Example:
|
|
|
136
319
|
|
|
137
320
|
Ordered formatter chains keyed by file extension.
|
|
138
321
|
|
|
139
|
-
|
|
140
|
-
If
|
|
322
|
+
No default chains are shipped — formatting is fully opt-in.
|
|
323
|
+
If no `chains` are declared, `pi-autoformat` does not run any formatter for any file.
|
|
324
|
+
This avoids surprises from a default formatter (e.g. prettier) conflicting with the project's chosen tool (e.g. biome).
|
|
141
325
|
|
|
142
326
|
The chain order is explicit and should be preserved.
|
|
143
327
|
|
|
328
|
+
A chain entry is an array of *steps*.
|
|
329
|
+
Each step is one of:
|
|
330
|
+
|
|
331
|
+
- a formatter name (string) — runs that formatter (current behavior).
|
|
332
|
+
- a fallback group (`{ "fallback": [name, name, ...] }`) — runs the first listed formatter whose command is on `PATH`.
|
|
333
|
+
|
|
144
334
|
Example:
|
|
145
335
|
|
|
146
336
|
```json
|
|
147
337
|
{
|
|
148
338
|
"chains": {
|
|
149
|
-
".
|
|
150
|
-
".
|
|
151
|
-
".
|
|
339
|
+
".ts": ["biome"],
|
|
340
|
+
".tsx": ["biome"],
|
|
341
|
+
".json": ["biome"],
|
|
342
|
+
".md": ["markdownlint-cli2"]
|
|
152
343
|
}
|
|
153
344
|
}
|
|
154
345
|
```
|
|
155
346
|
|
|
347
|
+
Fallback example:
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"chains": {
|
|
352
|
+
".ts": [{ "fallback": ["biome", "prettier"] }],
|
|
353
|
+
".tsx": [{ "fallback": ["biome", "prettier"] }],
|
|
354
|
+
".md": [
|
|
355
|
+
{ "fallback": ["biome", "prettier"] },
|
|
356
|
+
"markdownlint-cli2"
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### Fallback semantics
|
|
363
|
+
|
|
364
|
+
The only fallthrough trigger is **command not found in `PATH`**.
|
|
365
|
+
Non-zero exit codes are treated as real failures and surfaced — they are
|
|
366
|
+
not masked by trying the next alternative.
|
|
367
|
+
|
|
368
|
+
| Outcome of formatter N in the group | Behavior |
|
|
369
|
+
| ----------------------------------- | ----------------------------------------------------- |
|
|
370
|
+
| Command not on `PATH` | Skip, try N+1 |
|
|
371
|
+
| Command runs, exits 0 | Success, stop the group |
|
|
372
|
+
| Command runs, exits non-zero | Failure, stop the group, report |
|
|
373
|
+
| All formatters missing from `PATH` | Group is a no-op (no batch run emitted) |
|
|
374
|
+
|
|
375
|
+
The `PATH` probe is cached per flush, so the same command is probed at
|
|
376
|
+
most once across a single agent turn even when many extensions share
|
|
377
|
+
the same fallback group.
|
|
378
|
+
|
|
379
|
+
When a non-first alternative wins, the formatter name in success and
|
|
380
|
+
failure summaries is annotated with which earlier alternatives were
|
|
381
|
+
skipped (e.g. `prettier (fallback after biome unavailable)`).
|
|
382
|
+
|
|
383
|
+
#### Choosing a chain strategy
|
|
384
|
+
|
|
385
|
+
Prefer **project-level** `chains` over relying on global fallback.
|
|
386
|
+
Global `chains` are convenient defaults, but become ambiguous in
|
|
387
|
+
repositories that use multiple alternative tools.
|
|
388
|
+
A project-level `chains` declaration in
|
|
389
|
+
`.pi/extensions/pi-autoformat/config.json` is explicit, predictable,
|
|
390
|
+
and survives team handoffs.
|
|
391
|
+
|
|
392
|
+
Treat global fallback (`[{ "fallback": ["biome", "prettier"] }]`) as a
|
|
393
|
+
"what to do when no project config has opinions" backstop — useful for
|
|
394
|
+
ad-hoc repos, not load-bearing for projects you maintain.
|
|
395
|
+
|
|
396
|
+
#### Fallback caveat
|
|
397
|
+
|
|
398
|
+
Fallback chooses the first formatter whose command is on `PATH`.
|
|
399
|
+
It does **not** check whether the tool has a project config to apply.
|
|
400
|
+
A globally installed Biome will win a `[biome, prettier]` fallback even
|
|
401
|
+
in repos that use Prettier — and Biome will format the file with its
|
|
402
|
+
built-in defaults.
|
|
403
|
+
If both alternatives are realistic in your environment, declare a
|
|
404
|
+
project-level chain to disambiguate.
|
|
405
|
+
|
|
406
|
+
#### Wildcard chain key (`*`)
|
|
407
|
+
|
|
408
|
+
In addition to per-extension keys, `chains` may declare a single `"*"`
|
|
409
|
+
entry that applies to **every** touched file (including files without
|
|
410
|
+
an extension).
|
|
411
|
+
The wildcard chain runs first across the full batch.
|
|
412
|
+
Files that any built-in dispatcher (see [built-in formatters](#built-in-formatters)
|
|
413
|
+
below) reports as unhandled fall through to the per-extension chain
|
|
414
|
+
for their extension; files claimed by the wildcard chain are removed
|
|
415
|
+
from the per-extension pass to avoid double-formatting.
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
{
|
|
419
|
+
"chains": {
|
|
420
|
+
"*": [{ "fallback": ["treefmt-nix", "treefmt"] }],
|
|
421
|
+
".ts": [{ "fallback": ["biome", "prettier"] }],
|
|
422
|
+
".md": ["prettier", "markdownlint-cli2"]
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
This pattern lets a project-level dispatcher (`treefmt` or
|
|
428
|
+
`treefmt-nix`) handle anything it knows about, while per-extension
|
|
429
|
+
chains backstop the rest.
|
|
430
|
+
|
|
431
|
+
#### Built-in formatters
|
|
432
|
+
|
|
433
|
+
Two formatter names are shipped as built-ins and may be referenced in
|
|
434
|
+
`chains` without a `formatters` entry:
|
|
435
|
+
|
|
436
|
+
- `treefmt` — discovers `treefmt.toml` (preferred) or `.treefmt.toml`
|
|
437
|
+
by walking up from each touched file, then invokes
|
|
438
|
+
`treefmt --config-file <found> -- <paths...>` from the discovered
|
|
439
|
+
root.
|
|
440
|
+
- `treefmt-nix` — discovers `flake.nix` together with `treefmt.nix`
|
|
441
|
+
(or `nix/treefmt.nix`) by walking up from each touched file, then
|
|
442
|
+
invokes
|
|
443
|
+
`nix fmt --no-update-lock-file --no-write-lock-file -- <paths...>`
|
|
444
|
+
from the flake root.
|
|
445
|
+
|
|
446
|
+
Discovered config-root paths are cached for the lifetime of the
|
|
447
|
+
autoformatter, so repeated flushes within a session do not re-walk the
|
|
448
|
+
filesystem.
|
|
449
|
+
|
|
450
|
+
Both built-ins translate documented "no formatter for path" output into
|
|
451
|
+
a clean **skip** outcome so chain composition (especially `fallback`
|
|
452
|
+
and the wildcard-then-per-extension flow) works naturally:
|
|
453
|
+
|
|
454
|
+
- `treefmt`: stderr lines matching `no formatter for path: <p>` mark
|
|
455
|
+
that file as unhandled.
|
|
456
|
+
An exit-0 run where every input file was unhandled is treated as a
|
|
457
|
+
full skip.
|
|
458
|
+
- `treefmt-nix`: stderr containing `emitted 0 files for processing`
|
|
459
|
+
is treated as a full skip; transient `nix` daemon errors
|
|
460
|
+
(e.g. `cannot connect to socket`) are also skipped so a downstream
|
|
461
|
+
fallback alternative can take over.
|
|
462
|
+
|
|
463
|
+
Anything else with a non-zero exit is reported as a real failure and is
|
|
464
|
+
never silently swallowed.
|
|
465
|
+
|
|
466
|
+
When both `treefmt` and `treefmt-nix` appear inside the same `fallback`
|
|
467
|
+
group and both are on `PATH` and both resolve to a config at the **same**
|
|
468
|
+
root, `treefmt-nix` wins regardless of declaration order.
|
|
469
|
+
When the roots differ, the user-declared order is preserved.
|
|
470
|
+
|
|
471
|
+
Declaring a `formatters` entry whose key matches a built-in name still
|
|
472
|
+
works — the user-declared definition wins, providing an escape hatch for
|
|
473
|
+
custom flags — but the loader emits a single non-fatal config issue so
|
|
474
|
+
the shadowing is visible.
|
|
475
|
+
|
|
156
476
|
## Merge behavior
|
|
157
477
|
|
|
158
478
|
Merge order:
|
|
159
479
|
|
|
160
|
-
1. built-in defaults
|
|
480
|
+
1. built-in defaults (scalar settings only — no default chains are shipped)
|
|
161
481
|
2. global config
|
|
162
482
|
3. project config
|
|
163
483
|
|
|
164
484
|
Recommended merge semantics:
|
|
165
485
|
|
|
166
486
|
- top-level scalar values override by precedence
|
|
167
|
-
- `formatters` merge by formatter name
|
|
168
|
-
- `chains` merge by extension key
|
|
487
|
+
- `formatters` merge by formatter name (built-in `prettier` and `markdownlint-cli2` definitions are available for convenience but inert without chains)
|
|
488
|
+
- `chains` merge by extension key — no built-in chains exist, so only user-declared chains take effect
|
|
169
489
|
- when a project config defines a formatter or chain key, that key replaces the lower-precedence value for that entry
|
|
170
490
|
|
|
171
|
-
This keeps repo-local formatter behavior explicit while still allowing users to set global defaults such as `
|
|
491
|
+
This keeps repo-local formatter behavior explicit while still allowing users to set global defaults such as `commandTimeoutMs`.
|
|
172
492
|
|
|
173
493
|
## Notes
|
|
174
494
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue_title: "Initial implementation plan (predates issue tracking)"
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Pi Autoformat: Initial Implementation Plan
|
|
2
6
|
|
|
3
7
|
## Problem Statement
|
|
4
8
|
|
|
5
|
-
Pi agents frequently modify files that later fail commit-time hooks because formatting was not run after editing.
|
|
9
|
+
Pi agents frequently modify files that later fail commit-time hooks because formatting was not run after editing.
|
|
10
|
+
This is especially painful for formatters that mutate files, such as Prettier and Markdown lint fixers.
|
|
6
11
|
|
|
7
12
|
In practice, this creates a bad workflow:
|
|
8
13
|
|
|
@@ -335,13 +340,14 @@ Completed:
|
|
|
335
340
|
|
|
336
341
|
### Phase 7: optional enhancements
|
|
337
342
|
|
|
338
|
-
|
|
343
|
+
Status update:
|
|
339
344
|
|
|
340
|
-
- session mode
|
|
341
|
-
- tool mode
|
|
342
|
-
- support for more mutation tools
|
|
343
|
-
-
|
|
344
|
-
-
|
|
345
|
+
- session mode — implemented (flush on `session_shutdown`)
|
|
346
|
+
- tool mode — implemented
|
|
347
|
+
- support for more mutation tools — implemented via `customMutationTools` config; arbitrary tool names with dotted `pathField` / `pathFields` specs feed the touched-files queue.
|
|
348
|
+
- shell mutation integration strategy — implemented per [docs/plans/0004-shell-driven-mutation-coverage.md](./0004-shell-driven-mutation-coverage.md) with three opt-in strategies (argument parsing, snapshot tracking, user-declared wrappers) plus a uniform `formatScope` boundary.
|
|
349
|
+
- EventBus integration — implemented via `eventBusMutationChannel` (default `autoformat:touched`); peer extensions can publish `{ path }` or `{ paths }` payloads to opt their own mutations into the formatter pipeline.
|
|
350
|
+
- optional settings command / config editor UI — not yet started.
|
|
345
351
|
|
|
346
352
|
## Remaining Work Summary
|
|
347
353
|
|
|
@@ -377,8 +383,10 @@ Mitigation:
|
|
|
377
383
|
|
|
378
384
|
Mitigation:
|
|
379
385
|
|
|
380
|
-
-
|
|
381
|
-
-
|
|
386
|
+
- shipped opt-in shell mutation detection with three explicit strategies (see plan 0004)
|
|
387
|
+
- exposed `customMutationTools` for project-specific tool names
|
|
388
|
+
- exposed `eventBusMutationChannel` so peer extensions can contribute touched files without us modeling their tools
|
|
389
|
+
- all mutation sources funnel through the same `TouchedFilesQueue` and `formatScope` filter, keeping behavior auditable
|
|
382
390
|
|
|
383
391
|
### Risk: formatter failures become invisible
|
|
384
392
|
|