@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.
Files changed (75) hide show
  1. package/.github/workflows/ci.yml +1 -3
  2. package/.github/workflows/release-please.yml +29 -0
  3. package/.markdownlint-cli2.yaml +14 -2
  4. package/.pi/extensions/pi-autoformat/config.json +3 -6
  5. package/.pi/prompts/README.md +59 -0
  6. package/.pi/prompts/plan-issue.md +64 -0
  7. package/.pi/prompts/retro.md +144 -0
  8. package/.pi/prompts/ship-issue.md +77 -0
  9. package/.pi/prompts/tdd-plan.md +67 -0
  10. package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
  11. package/.release-please-manifest.json +1 -1
  12. package/AGENTS.md +39 -0
  13. package/CHANGELOG.md +365 -0
  14. package/README.md +42 -109
  15. package/biome.json +1 -1
  16. package/docs/assets/logo.png +0 -0
  17. package/docs/assets/logo.svg +533 -0
  18. package/docs/configuration.md +358 -38
  19. package/docs/plans/0001-initial-implementation-plan.md +17 -9
  20. package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
  21. package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
  22. package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
  23. package/docs/plans/0010-acceptance-test-coverage.md +240 -0
  24. package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
  25. package/docs/plans/0013-fallback-chain-step-type.md +280 -0
  26. package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
  27. package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
  28. package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
  29. package/docs/plans/0022-pi-coding-agent-types.md +201 -0
  30. package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
  31. package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
  32. package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
  33. package/docs/retro/0013-fallback-chain-step-type.md +67 -0
  34. package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
  35. package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
  36. package/docs/retro/0022-pi-coding-agent-types.md +62 -0
  37. package/docs/testing.md +95 -0
  38. package/package.json +30 -11
  39. package/prek.toml +2 -2
  40. package/schemas/pi-autoformat.schema.json +145 -21
  41. package/src/builtin-formatters.ts +205 -0
  42. package/src/command-probe.ts +66 -0
  43. package/src/config-loader.ts +829 -90
  44. package/src/custom-mutation-tools.ts +125 -0
  45. package/src/extension.ts +469 -82
  46. package/src/format-scope.ts +118 -0
  47. package/src/formatter-config.ts +73 -36
  48. package/src/formatter-executor.ts +230 -34
  49. package/src/formatter-output-report.ts +149 -0
  50. package/src/formatter-registry.ts +139 -30
  51. package/src/index.ts +26 -5
  52. package/src/prompt-autoformatter.ts +148 -23
  53. package/src/shell-mutation-detector.ts +572 -0
  54. package/src/touched-files-queue.ts +72 -11
  55. package/test/acceptance-event-bus.test.ts +138 -0
  56. package/test/acceptance.test.ts +69 -0
  57. package/test/builtin-formatters.test.ts +382 -0
  58. package/test/command-probe.test.ts +79 -0
  59. package/test/config-loader.test.ts +640 -21
  60. package/test/custom-mutation-tools.test.ts +190 -0
  61. package/test/extension.test.ts +1535 -158
  62. package/test/fallback-acceptance.test.ts +98 -0
  63. package/test/fixtures/event-bus-emitter.ts +26 -0
  64. package/test/fixtures/formatter-recorder.mjs +25 -0
  65. package/test/format-scope.test.ts +139 -0
  66. package/test/formatter-config.test.ts +56 -5
  67. package/test/formatter-executor.test.ts +555 -35
  68. package/test/formatter-output-report.test.ts +178 -0
  69. package/test/formatter-registry.test.ts +330 -37
  70. package/test/helpers/rpc.ts +146 -0
  71. package/test/prompt-autoformatter.test.ts +315 -22
  72. package/test/schema.test.ts +149 -0
  73. package/test/shell-mutation-detector.test.ts +221 -0
  74. package/test/touched-files-queue.test.ts +40 -1
  75. package/test/types/theme-stub.test-d.ts +42 -0
@@ -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
- "prettier": {
35
- "command": ["prettier", "--write", "$FILE"],
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
- ".md": ["prettier", "markdownlint-cli2"],
45
- ".ts": ["prettier"],
46
- ".tsx": ["prettier"]
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/v1.0.0/schemas/pi-autoformat.schema.json"
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
- ### `formatMode`
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
- When formatting should run.
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
- - `"prompt"` — format once after the agent finishes the prompt. Recommended default.
68
- - `"tool"` format immediately after each successful mutation tool call.
69
- - `"session"` accumulate touched files and format on session shutdown.
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
- ### `commandTimeoutMs`
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
- Timeout in milliseconds for each formatter command.
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
- "commandTimeoutMs": 10000
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
- `$FILE` is replaced with the absolute path to the touched file.
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
- For v1, formatter command resolution stays intentionally simple:
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", "$FILE"],
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", "$FILE"],
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
- For v1, formatter execution is driven by explicit `chains` only.
140
- If an extension has no `chains` entry, `pi-autoformat` does not run any formatter for that extension.
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
- ".md": ["prettier", "markdownlint-cli2"],
150
- ".ts": ["prettier"],
151
- ".tsx": ["prettier"]
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 `formatMode`.
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. This is especially painful for formatters that mutate files, such as Prettier and Markdown lint fixers.
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
- Still optional and not yet started:
343
+ Status update:
339
344
 
340
- - session mode
341
- - tool mode
342
- - support for more mutation tools
343
- - optional shell mutation integration strategy
344
- - optional settings command / config editor UI
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
- - treat as a known limitation in v1
381
- - design extension internals so more mutation sources can be added later
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