@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
@@ -0,0 +1,290 @@
1
+ ---
2
+ issue: 15
3
+ issue_title: "Built-in `treefmt` and `treefmt-nix` project formatter support"
4
+ ---
5
+
6
+ # Plan: Built-in `treefmt` and `treefmt-nix` Project Formatter Support (Issue #15)
7
+
8
+ ## Problem Statement
9
+
10
+ `treefmt` and `treefmt-nix` are widely used project-level formatter dispatchers.
11
+ A repository declares its entire formatter chain once in `treefmt.toml` (or in `treefmt.nix` inside a flake), and a single CLI call routes each path to the right formatter.
12
+ Today, users of these tools must redeclare every formatter in `pi-autoformat`'s `formatters` and `chains`, which duplicates and drifts from the project's source of truth.
13
+
14
+ This plan adds two opt-in built-in formatters — `treefmt` and `treefmt-nix` — that users can reference in `chains` without declaring them in `formatters`.
15
+ A new wildcard chain key (`"*"`) expresses "applies to any extension" so a single chain entry can hand the whole batch to the project-level dispatcher first, with per-extension chains backstopping any path the dispatcher does not handle.
16
+
17
+ ## Goals
18
+
19
+ - Ship two built-in formatter names usable in `chains` without a `formatters` entry: `treefmt` and `treefmt-nix`.
20
+ - Discover `treefmt.toml` / `.treefmt.toml` (for `treefmt`) and `flake.nix` + `treefmt.nix` / `nix/treefmt.nix` (for `treefmt-nix`) by walking up from each touched file.
21
+ - Cache discovered config-root paths per session.
22
+ - Support a wildcard `"*"` chain key that runs first against the full batch, with files punted by the wildcard chain falling through to their per-extension chain.
23
+ - Map documented "no formatter matched this path" outputs to a clean **skip** outcome so `fallback` chain steps and per-extension fallthrough compose naturally.
24
+ - Keep all other non-zero exits visible as real failures.
25
+ - Keep this strictly opt-in: zero behavioral change unless a user references one of the built-ins or `"*"`.
26
+
27
+ ## Non-Goals
28
+
29
+ - Adding general-purpose "named built-in formatters" beyond these two.
30
+ Future built-ins can reuse the same plumbing but are out of scope here.
31
+ - Introducing arbitrary glob keys (`"*.tsx.snap"`, `"src/**"`).
32
+ Only the literal `"*"` token is supported.
33
+ - Wrapping arbitrary `treefmt` flags through config.
34
+ The two built-ins ship with fixed argv shapes; users who need custom flags can declare a regular `formatters` entry instead.
35
+ - Detecting which specific formatter inside `treefmt` failed.
36
+ We treat `treefmt` as one batch step; per-formatter attribution stays inside `treefmt`'s own output.
37
+ - Parallelizing the wildcard chain and per-extension chain.
38
+ Wildcard runs first, per-extension chains run after, on whatever files remain.
39
+
40
+ ## Background
41
+
42
+ Relevant existing modules:
43
+
44
+ - `src/formatter-registry.ts` — defines `ChainStep`, `ResolvedChainStep`, `ResolvedFormatter`, `groupFilesByChain`, and `resolveChainSteps`.
45
+ Today `groupFilesByChain` keys purely on `path.extname(filePath).toLowerCase()` and silently drops files without an extension or without a matching chain entry.
46
+ - `src/formatter-executor.ts` — `executeChainGroup` runs each `ResolvedChainStep` once over the group's full file list.
47
+ `fallback` step currently uses a synchronous `CommandProbe` (PATH check) to choose one alternative.
48
+ - `src/prompt-autoformatter.ts` — orchestrates flush: queue → group → resolve → execute.
49
+ - `src/formatter-config.ts` — defaults and merge.
50
+ - `src/command-probe.ts` — cached PATH probe used per flush.
51
+ - `schemas/pi-autoformat.schema.json` — currently restricts chain keys to `^\..+` (extension-style only).
52
+ - `docs/configuration.md`, `README.md` — kept aligned.
53
+
54
+ The recently landed dependencies make this issue cheap:
55
+
56
+ - #12 (`extensions` field removed) — chains alone drive dispatch.
57
+ - #13 (`fallback` step) — composes naturally with the new built-ins.
58
+ - #14 (batch dispatch) — `treefmt` only makes sense with batched paths; per-file would defeat the point.
59
+
60
+ ## Design Overview
61
+
62
+ ### Built-in formatter shape
63
+
64
+ Built-ins are not user-declared in `formatters`.
65
+ They are referenced by name (`"treefmt"`, `"treefmt-nix"`) inside `chains`.
66
+ The registry resolver (`resolveChainSteps`) consults a built-in table when a name is not present in user/default `formatters`.
67
+ Built-ins resolve to a richer descriptor than ordinary formatters because their command depends on per-flush context (discovered config root) and their outcome partitioning depends on parsing the dispatcher's output.
68
+
69
+ ```typescript
70
+ export type BuiltinFormatter = {
71
+ name: string;
72
+ /**
73
+ * Discover the dispatcher's config root by walking up from each touched
74
+ * file's directory. Returns undefined when no config applies, in which
75
+ * case the built-in is skipped for that flush. Cached per session.
76
+ */
77
+ discoverRoot(files: string[]): Promise<string | undefined>;
78
+ /** Build the argv to invoke given the discovered root and the file batch. */
79
+ buildCommand(root: string, files: string[]): { command: string[]; cwd: string };
80
+ /**
81
+ * Inspect a completed BatchRun and return the subset of input files the
82
+ * dispatcher reported as "no formatter matched". Those files fall through
83
+ * to subsequent chain steps / per-extension chains.
84
+ */
85
+ partitionUnhandled(run: BatchRun, files: string[]): {
86
+ handled: string[];
87
+ unhandled: string[];
88
+ /** When the entire run is a documented "skip" (e.g. nix transient error). */
89
+ treatAsSkip: boolean;
90
+ };
91
+ };
92
+ ```
93
+
94
+ `ResolvedFormatter` gains an optional `builtin?: BuiltinFormatter` discriminator.
95
+ When set, the executor takes the built-in path; otherwise behavior is unchanged.
96
+
97
+ ### Wildcard `"*"` chain key
98
+
99
+ `groupFilesByChain` is extended:
100
+
101
+ 1. If `chains["*"]` exists, all touched files are first considered for the wildcard chain regardless of extension (including extensionless files).
102
+ 2. The wildcard pass produces a `ChainGroupResult` with the full batch and tracks per-step **handled vs. unhandled** sets via `BuiltinFormatter.partitionUnhandled`.
103
+ 3. After the wildcard pass, only files that ended up unhandled by the wildcard (every step either skipped them or never ran) flow into the existing per-extension grouping.
104
+ 4. Files handled by the wildcard chain do not appear in any per-extension group.
105
+
106
+ For non-built-in formatters in a `"*"` chain, every file is considered "handled" (the same as today's per-extension chain semantics). This keeps the wildcard generic but makes it most useful with built-ins or fallback groups containing built-ins.
107
+
108
+ ### Discovery and caching
109
+
110
+ A per-session cache keyed by starting directory maps to the discovered config-root path or a "no match" sentinel.
111
+ The cache lives on the `PromptAutoformatter` instance (so it survives across flushes within a session) and is passed to built-ins via the existing options plumbing.
112
+
113
+ Walk strategy: from `path.dirname(filePath)`, walk up to the filesystem root, returning the first directory containing the relevant config file(s).
114
+ When the same directory is visited from multiple files, the cache short-circuits.
115
+
116
+ ### Precedence between `treefmt` and `treefmt-nix`
117
+
118
+ When both apply at the same root, prefer `treefmt-nix`.
119
+ This precedence is enforced *only* when both names appear inside the same `fallback` group (or sequential chain) — we do not silently override user ordering elsewhere.
120
+ Mechanism: inside a `fallback` group, after PATH probing, if both `treefmt-nix` and `treefmt` are viable and resolve to configs at the same root, `treefmt-nix` wins regardless of order.
121
+
122
+ ### Skip detection
123
+
124
+ Two narrowly-scoped patterns, mirroring `pi-formatter`:
125
+
126
+ - `treefmt`: stderr line matching `/no formatter for path[: ]+(?<path>\S+)/` per path → that path is unhandled.
127
+ Exit code 0 with all paths unhandled → treat as skip.
128
+ - `treefmt-nix`: stderr containing `emitted 0 files for processing` → entire run is skip.
129
+ - Transient nix daemon / sandbox errors detected via known substrings (e.g. `error: cannot connect to socket`, `error: build of`) → entire run is skip so fallback can try the next alternative.
130
+
131
+ Anything else with a non-zero exit is a real failure and is reported normally.
132
+ The exact regex/substring set lives in one place and is unit-tested.
133
+
134
+ ### Edge cases
135
+
136
+ - Wildcard chain references an undeclared, non-builtin name → loader emits a single config-issue, the entry is dropped (existing pattern).
137
+ - Wildcard chain has zero applicable files (e.g. all files were filtered by scope) → no run.
138
+ - Built-in's discovery returns no root → step skipped, all files flow to next step / per-extension chains.
139
+ - `tool` mode: batch is size 1; built-in still works (single path passed to dispatcher).
140
+ - File path is outside the discovered config root → still passed to the dispatcher; treefmt itself decides.
141
+ We do not pre-filter by root, but discovery uses each file's own directory so multi-root repos handle naturally.
142
+
143
+ ## Module-Level Changes
144
+
145
+ ### `src/formatter-registry.ts`
146
+
147
+ - Extend `ResolvedFormatter` with optional `builtin?: BuiltinFormatter`.
148
+ - Update `groupFilesByChain` to accept a wildcard chain (`chains["*"]`) and produce a wildcard group ahead of per-extension groups.
149
+ - Allow files without an extension when the wildcard chain is set.
150
+ - Adjust the chain encoder so the wildcard group is keyed distinctly.
151
+
152
+ ### `src/builtin-formatters.ts` (new)
153
+
154
+ - Export a `BUILTIN_FORMATTERS` registry: `Record<string, BuiltinFormatter>`.
155
+ - Implement `treefmt` and `treefmt-nix`:
156
+ - discovery walkers
157
+ - command builders (`treefmt --config-file <root>/treefmt.toml --` and `nix fmt --no-update-lock-file --no-write-lock-file --`)
158
+ - output partitioners (skip-pattern matching)
159
+ - Export discovery cache type.
160
+
161
+ ### `src/formatter-executor.ts`
162
+
163
+ - New `executeChainGroup` variant that, for steps containing a built-in formatter, threads the working `files` set through `partitionUnhandled` and returns the **unhandled tail** alongside `BatchRun[]`.
164
+ - Honor `treatAsSkip` (do not record the step as a failed run).
165
+ - Keep current behavior intact for non-built-in steps.
166
+ - Inside `fallback` groups, apply the `treefmt-nix` preference rule when applicable.
167
+
168
+ ### `src/prompt-autoformatter.ts`
169
+
170
+ - Run the wildcard chain first against the full touched-files batch.
171
+ - Subtract the wildcard's "handled" set; pass the remainder to per-extension grouping.
172
+ - Hold a session-scoped discovery cache and pass it into the executor.
173
+
174
+ ### `src/formatter-config.ts`
175
+
176
+ - No changes to defaults (built-ins are opt-in by reference).
177
+ - `chains` type continues to be `Record<string, ChainStep[]>` — `"*"` is just a key.
178
+ - Loader: validate that any string referenced in a chain is either a declared formatter or a built-in name.
179
+
180
+ ### `schemas/pi-autoformat.schema.json`
181
+
182
+ - Loosen the `chains` `propertyNames` pattern to also allow the literal `"*"`: `^(\.|\*$).+|^\*$`.
183
+ - Update the `chains` description to document `"*"` and the built-in names.
184
+
185
+ ### `docs/configuration.md` and `README.md`
186
+
187
+ - Document `"*"` semantics (run-first, fallthrough on skip).
188
+ - Document the two built-ins, their discovery rules, the `treefmt-nix` precedence, the canonical fallback example, and the skip-pattern policy.
189
+ - Note: built-ins do not need a `formatters` entry; declaring one with the same name shadows the built-in (loader emits a config issue if the user shadows a built-in).
190
+
191
+ ### Tests
192
+
193
+ - `test/builtin-formatters.test.ts` (new): discovery walkers, command builders, output parsers.
194
+ - `test/formatter-registry.test.ts`: wildcard grouping, file-without-extension behavior, wildcard + per-extension interaction.
195
+ - `test/formatter-executor.test.ts`: built-in step with mixed handled/unhandled, `treatAsSkip` paths, `treefmt-nix` precedence inside a fallback group.
196
+ - `test/prompt-autoformatter.test.ts`: wildcard runs first, unhandled files fall through to per-extension chains, handled files do not double-format.
197
+ - `test/schema.test.ts`: `"*"` accepted as a chain key.
198
+ - `test/config-loader.test.ts`: built-in names accepted in chains without a `formatters` entry; shadow-built-in produces a config issue.
199
+
200
+ ## TDD Order
201
+
202
+ Each cycle is a tight red→green→commit. Numbering restarts at 1 under this heading.
203
+
204
+ 1. **Schema accepts `"*"` chain key.**
205
+ Surface: `test/schema.test.ts`.
206
+ Cover: `chains: { "*": [...] }` validates; legacy patterns continue to validate.
207
+ Commit: `test: accept "*" chain key in schema`, then `feat: allow wildcard chain key in schema`.
208
+
209
+ 2. **Wildcard grouping.**
210
+ Surface: `test/formatter-registry.test.ts`.
211
+ Cover: `groupFilesByChain` produces a wildcard-first group when `chains["*"]` is set; extensionless files included; per-extension groups still produced.
212
+ Commit: `test: cover wildcard chain grouping`, then `feat: group files by wildcard chain first`.
213
+
214
+ 3. **Built-in resolution without a `formatters` entry.**
215
+ Surface: `test/formatter-registry.test.ts` + new `test/builtin-formatters.test.ts`.
216
+ Cover: `resolveChainSteps` resolves `treefmt` / `treefmt-nix` to a `ResolvedFormatter` with `builtin` set, even when `formatters` is empty.
217
+ Commit: `test: resolve built-in formatter names without registry entry`, then `feat: register treefmt and treefmt-nix as built-in formatters`.
218
+
219
+ 4. **Discovery walkers.**
220
+ Surface: `test/builtin-formatters.test.ts` against fixture directories under `test/fixtures/`.
221
+ Cover: walks up from a file path, finds `treefmt.toml`, `.treefmt.toml`, `flake.nix` + `treefmt.nix`, `flake.nix` + `nix/treefmt.nix`; returns `undefined` when no config; cache hits do not re-walk.
222
+ Commit: `test: cover treefmt config discovery`, then `feat: discover treefmt and treefmt-nix config roots`.
223
+
224
+ 5. **Command builders.**
225
+ Surface: `test/builtin-formatters.test.ts`.
226
+ Cover: `treefmt` argv shape with `--config-file`; `treefmt-nix` argv shape with `nix fmt --no-update-lock-file --no-write-lock-file --` from the flake root; `cwd` set to the discovered root.
227
+ Commit: `test: cover treefmt command builders`, then `feat: build treefmt and treefmt-nix invocations`.
228
+
229
+ 6. **Skip-pattern parsing.**
230
+ Surface: `test/builtin-formatters.test.ts`.
231
+ Cover: `partitionUnhandled` for `treefmt` parses "no formatter for path" lines into the unhandled set; `treefmt-nix` "emitted 0 files for processing" → `treatAsSkip`; transient nix errors → `treatAsSkip`; unknown non-zero exit → real failure (no skip).
232
+ Commit: `test: cover built-in skip-pattern parsing`, then `feat: parse treefmt skip patterns`.
233
+
234
+ 7. **Executor honors built-in partitioning.**
235
+ Surface: `test/formatter-executor.test.ts`.
236
+ Cover: when a step is built-in and reports unhandled files, those files flow to the next step's input; `treatAsSkip` does not record a failed run; non-skip non-zero exit *is* recorded.
237
+ Commit: `test: thread built-in partitioning through executor`, then `feat: partition built-in batches by handled set`.
238
+
239
+ 8. **`treefmt-nix` precedence inside a fallback group.**
240
+ Surface: `test/formatter-executor.test.ts`.
241
+ Cover: when both built-ins are PATH-available and resolve to configs at the same root, `treefmt-nix` wins regardless of declaration order.
242
+ Commit: `test: cover treefmt-nix precedence in fallback`, then `feat: prefer treefmt-nix over treefmt at same root`.
243
+
244
+ 9. **Wildcard-then-per-extension flow.**
245
+ Surface: `test/prompt-autoformatter.test.ts`.
246
+ Cover: wildcard chain runs first across the full batch; files marked handled by the wildcard are removed from per-extension groups; files marked unhandled flow to per-extension chains; double-formatting is avoided.
247
+ Commit: `test: cover wildcard-then-per-extension dispatch`, then `feat: dispatch wildcard chain before per-extension chains`.
248
+
249
+ 10. **Loader validation for built-in names.**
250
+ Surface: `test/config-loader.test.ts`.
251
+ Cover: built-in names are accepted in chains without a `formatters` declaration; declaring a `formatters` entry that shadows a built-in name surfaces a single config issue; unknown names continue to surface a config issue.
252
+ Commit: `test: validate built-in formatter names in loader`, then `feat: accept built-in names in chains validation`.
253
+
254
+ 11. **Docs alignment.**
255
+ Surface: `docs/configuration.md`, `README.md`.
256
+ Cover: `"*"` semantics, the canonical `fallback` example combining `treefmt` and `treefmt-nix`, discovery rules, precedence note, skip-pattern policy.
257
+ Commit: `docs: document built-in treefmt and treefmt-nix support`.
258
+
259
+ ## Risks and Mitigations
260
+
261
+ - **Risk:** Skip-pattern matching is brittle — `treefmt` output format may change.
262
+ **Mitigation:** Patterns are centralized in one module with focused tests, easy to update.
263
+ Anything we cannot confidently classify defaults to "real failure" (visible), not "silent skip".
264
+
265
+ - **Risk:** Wildcard chain accidentally double-formats files when a built-in handles them and a per-extension chain runs anyway.
266
+ **Mitigation:** Wildcard-handled files are subtracted from the per-extension grouping. Tested explicitly.
267
+
268
+ - **Risk:** Discovery walks become a per-flush hot path on large repos with many touched files.
269
+ **Mitigation:** Per-session cache keyed by directory; the walk for each flush is at most O(unique-dirs × depth).
270
+
271
+ - **Risk:** `treefmt-nix` precedence rule could surprise users who explicitly listed `treefmt` first.
272
+ **Mitigation:** Precedence applies only when both resolve to the same root inside the same group; documented in `docs/configuration.md`.
273
+ Otherwise user order wins.
274
+
275
+ - **Risk:** Users shadow a built-in by declaring `formatters: { treefmt: ... }`.
276
+ **Mitigation:** Loader emits a config issue but accepts the user's definition (escape hatch for custom flags).
277
+ The user's entry wins; the built-in is bypassed for that name.
278
+
279
+ - **Risk:** `nix fmt` is slow and can dominate flush time on first run.
280
+ **Mitigation:** Out of scope for this issue; document the cost.
281
+ Users can keep `treefmt` (non-nix) in the fallback group ahead of `treefmt-nix` when they don't need flake-pinned formatters.
282
+
283
+ ## Open Questions
284
+
285
+ - Should the wildcard chain key be exactly `"*"`, or also `"**"`?
286
+ Sticking with `"*"` for now; `"**"` can alias to it later if users find it natural.
287
+ - Should we expose the built-in registry for project extensibility (user-declared "built-in like" entries with discovery + skip patterns)?
288
+ Defer until a second use case emerges.
289
+ - Should `treefmt --config-file` discovery prefer `.treefmt.toml` over `treefmt.toml` when both exist at the same root?
290
+ Plan: prefer `treefmt.toml` (matches `treefmt`'s own precedence). Confirm during step 4.
@@ -0,0 +1,245 @@
1
+ ---
2
+ issue: 2
3
+ issue_title: "Support optional detailed formatter output in reports"
4
+ ---
5
+
6
+ # Plan: Detailed Formatter Output on Failure (Issue #2)
7
+
8
+ ## Problem Statement
9
+
10
+ The executor already captures every formatter run's `stdout` and `stderr` on `BatchRun`, but the reporting layer discards it.
11
+ On a failure the user sees only `formatter (exit N): file1, file2` and has no way to see the parser error, lint message, or stack trace that the formatter actually printed.
12
+ That makes debugging a failed prompt-end format require re-running the formatter by hand, which defeats the point of letting the extension run it.
13
+
14
+ The issue asks for an opt-in config knob that surfaces formatter output without making the happy path noisy or undermining the v1 "non-blocking, concise" reporting stance.
15
+
16
+ ## Goals
17
+
18
+ - Add an opt-in config object that, when enabled, includes formatter output in the failure notification / log block.
19
+ - Default behavior is unchanged: no formatter output in any user-facing surface.
20
+ - Surface output **only for failed runs** (success runs stay quiet, even when the option is on).
21
+ - Truncate output to a small, predictable budget per run so a chatty formatter cannot flood the TUI.
22
+ - Keep the status-line and success paths untouched — this is a failure-debugging surface only.
23
+ - Apply identical truncation/format rules to TUI (`notify`) and non-TUI (`console.warn`) sinks.
24
+
25
+ This change is **not** breaking: a new optional config object with safe defaults, no schema renames, no result-shape changes.
26
+
27
+ ## Non-Goals
28
+
29
+ - Surfacing successful-run output (out of scope; the issue is debugging-focused).
30
+ - Streaming formatter output during the run.
31
+ Output appears in the post-flush summary like everything else.
32
+ - A separate widget / pane / file for full output.
33
+ If the truncated tail isn't enough, the user can re-run the formatter directly; we link to the captured `command` array in the existing report.
34
+ - Diverging interactive vs non-interactive *content*.
35
+ Both surfaces get the same truncated block; only the carrier (`notify` vs `console.warn`) differs, matching what `defaultReportFlushResult` already does.
36
+ - Changing strict-mode / blocking semantics (#6).
37
+ Failures stay non-blocking; this only changes how loud they are.
38
+ - Persisting full output to disk (e.g. `.pi/extensions/pi-autoformat/last-run.log`).
39
+ Defer until someone asks; mechanism is forever.
40
+ - Exposing `BatchRun.stdout`/`stderr` on any new public type.
41
+ We read what the executor already captures.
42
+
43
+ ## Background
44
+
45
+ Relevant existing pieces:
46
+
47
+ - `src/formatter-executor.ts`
48
+ - `BatchRun` already carries `stdout?: string` and `stderr?: string` for every run, populated by `runner` (see `createCommandRunner` in `src/extension.ts`, which captures both via `execFileAsync`).
49
+ - On exec failure, `normalizeExecError` falls back to `error.message` for `stderr`, so we always have *something* to print on a failure.
50
+ - `src/extension.ts`
51
+ - `summarizeFailures(result)` builds the per-batch failure lines but ignores `run.stdout` / `run.stderr`.
52
+ - `buildLegacyFailureMessage(summary)` joins those lines into the multi-line block passed to `reportMessage(ctx, message, "warning")`.
53
+ - `reportMessage` routes to `ctx.ui.notify(..., "warning")` on TUI and `console.warn` otherwise.
54
+ - `formatStatusLine` builds the footer text.
55
+ It must not change.
56
+ - `src/formatter-config.ts`
57
+ - `AutoformatConfig` and `DEFAULT_FORMATTER_CONFIG` are the single source of truth for runtime config.
58
+ - Pattern for nested config objects with partial-merge already exists for `eventBusMutationChannel` and `shellMutationDetection`; we follow the same shape.
59
+ - `schemas/pi-autoformat.schema.json` — top-level keys with `additionalProperties: false`.
60
+ New key needs a `$defs` entry and a top-level property.
61
+ - `docs/configuration.md` and `README.md` — both list every public config key.
62
+
63
+ ## Design Overview
64
+
65
+ ### Config shape
66
+
67
+ A new top-level optional object `formatterOutput` controls failure-output surfacing.
68
+
69
+ ```typescript
70
+ export type FormatterOutputOnFailure = "none" | "stderr" | "both";
71
+
72
+ export type FormatterOutputReportingConfig = {
73
+ /** Which streams to include for *failed* runs. */
74
+ onFailure: FormatterOutputOnFailure;
75
+ /** Hard byte cap per stream per run (UTF-8 byte length). */
76
+ maxBytes: number;
77
+ /** Hard line cap per stream per run (after byte trimming). */
78
+ maxLines: number;
79
+ };
80
+
81
+ export const DEFAULT_FORMATTER_OUTPUT_REPORTING: FormatterOutputReportingConfig = {
82
+ onFailure: "none",
83
+ maxBytes: 4096,
84
+ maxLines: 40,
85
+ };
86
+ ```
87
+
88
+ Defaults preserve today's behavior (`onFailure: "none"` → nothing extra is printed, even for failures).
89
+ The two caps are advisory until `onFailure !== "none"`, but we keep them on the type unconditionally so users can tune them once and flip the switch.
90
+
91
+ `UserFormatterConfig` gains an optional `formatterOutput?: Partial<FormatterOutputReportingConfig>` and `createFormatterConfig` merges it via the same `{...default, ...user}` spread used for sibling objects.
92
+
93
+ ### Reporting surface
94
+
95
+ Only the *failure* notification body changes.
96
+ Status line, success body, and config-issue body are untouched.
97
+
98
+ Existing failure block (TUI notify / non-TUI `console.warn`):
99
+
100
+ ```text
101
+ Formatter failures in 1 batch:
102
+ prettier (exit 2): src/foo.ts
103
+ ```
104
+
105
+ With `formatterOutput.onFailure: "stderr"`:
106
+
107
+ ```text
108
+ Formatter failures in 1 batch:
109
+ prettier (exit 2): src/foo.ts
110
+ stderr:
111
+ src/foo.ts: SyntaxError: Unexpected token (3:11)
112
+ 1 | export const a = 1
113
+ 2 | export const b = 2
114
+ > 3 | export const = 3
115
+ | ^
116
+ ... 12 more lines
117
+ ```
118
+
119
+ With `"both"`, an analogous indented `stdout:` block appears immediately above `stderr:` (only when stdout is non-empty after trimming).
120
+ Empty streams are skipped entirely — no `stderr: (empty)` noise.
121
+
122
+ ### Truncation
123
+
124
+ Per run, per stream:
125
+
126
+ 1. Drop trailing whitespace.
127
+ 2. If `Buffer.byteLength(text, "utf8") > maxBytes`, keep the **last** `maxBytes` bytes, snap forward to the next newline boundary so we never bisect a multi-byte character mid-sequence, and prefix with `... (truncated, N earlier bytes)`.
128
+ The tail is what users want for a stack trace / parser error.
129
+ 3. If the resulting line count exceeds `maxLines`, keep the last `maxLines` lines and prefix with `... (truncated, N more lines)`.
130
+ 4. Indent every surviving line by 4 spaces under the `stderr:` / `stdout:` header (2-space header indent, 4-space body indent — matches existing failure-line conventions).
131
+
132
+ The byte cap runs first because some formatters emit megabyte-scale output; line cap is the secondary safety net for line-noisy output that fits under the byte cap.
133
+ Counts in the truncation marker reflect what was dropped from the original.
134
+
135
+ ### Edge cases
136
+
137
+ - **Stream undefined** (formatter never wrote): skip the corresponding block.
138
+ - **Stream non-empty but only whitespace**: skip.
139
+ - **`stdout` only on success**: never surfaced (we only annotate failed runs).
140
+ - **`onFailure: "none"`**: byte/line caps are irrelevant; do not even compute trimming.
141
+ - **Multiple failed runs in one flush**: each gets its own indented block under its own `formatter (exit N): files` line.
142
+ No interleaving.
143
+ - **Fallback runs**: if a fallback formatter fails, the existing `formatterLabel(name, fallbackContext)` line is unchanged; the output block sits beneath it.
144
+ - **Config issue: invalid `onFailure` value**: handled by `loadAutoformatConfig`'s existing schema-driven validation path, surfaces via `reportConfigIssues`, and the value falls back to the default.
145
+ - **Tiny `maxBytes` (e.g. `0` or `10`)**: respected; we render the truncation marker plus whatever fits.
146
+ Schema enforces `minimum: 0` for both caps.
147
+
148
+ ### Separation of concerns
149
+
150
+ The trimming/formatting logic lives in a new pure helper module so it is straightforward to unit test and so `extension.ts` doesn't grow another responsibility:
151
+
152
+ - `src/formatter-output-report.ts` exports `formatRunOutputBlock(run, config): string | undefined` returning the indented block (or `undefined` if nothing to print).
153
+ - `summarizeFailures` and `buildLegacyFailureMessage` in `src/extension.ts` accept the new config and call the helper per failed run.
154
+
155
+ ## Module-Level Changes
156
+
157
+ - `src/formatter-config.ts`
158
+ - Add `FormatterOutputOnFailure`, `FormatterOutputReportingConfig`, `DEFAULT_FORMATTER_OUTPUT_REPORTING`.
159
+ - Extend `AutoformatConfig` and `UserFormatterConfig` with `formatterOutput`.
160
+ - Merge in `createFormatterConfig` (object spread).
161
+ - `src/formatter-output-report.ts` (**new**)
162
+ - `formatRunOutputBlock(run, config)` — pure, no I/O, no Pi API surface.
163
+ - Internal helpers: `trimStream(text, { maxBytes, maxLines })`, `indentLines(text, prefix)`.
164
+ - `src/extension.ts`
165
+ - Thread `config.formatterOutput` into `summarizeFailures` (or a sibling that builds enriched lines).
166
+ - `buildLegacyFailureMessage` interleaves the per-run output blocks beneath each failure line.
167
+ - No change to `formatStatusLine` or any success path.
168
+ - `schemas/pi-autoformat.schema.json`
169
+ - Add `formatterOutput` property + `$defs/formatterOutputReportingConfig` with `onFailure` enum and `maxBytes` / `maxLines` integers (`minimum: 0`).
170
+ - `docs/configuration.md`
171
+ - New `### formatterOutput` section under Settings reference, with the default object, an example enabling `"stderr"`, and a note that successful runs are never surfaced.
172
+ - `README.md`
173
+ - Replace the "reporting is intentionally concise and does not yet expose full formatter stdout/stderr by default" line with a forward reference to the new option.
174
+ - `test/formatter-output-report.test.ts` (**new**)
175
+ - Trimming and indentation behavior; see TDD Order.
176
+ - `test/extension.test.ts`
177
+ - Add cases for the failure-block enrichment under each `onFailure` value.
178
+ - `test/formatter-config.test.ts` and `test/config-loader.test.ts`
179
+ - Cover defaults, partial merge, and schema validation of bad values.
180
+ - `test/schema.test.ts`
181
+ - Lock in the new schema entry and `additionalProperties: false` rejection of typos under `formatterOutput`.
182
+
183
+ ## TDD Order
184
+
185
+ 1. **test: cover trimStream byte and line caps**
186
+ - In a new `test/formatter-output-report.test.ts`, drive `trimStream` (or `formatRunOutputBlock` directly with crafted strings) through: empty string → `undefined`, whitespace-only → `undefined`, under both caps → unchanged, byte-cap exceeded → tail with marker, line-cap exceeded → tail with marker, multibyte boundary not bisected.
187
+ - Commit: `test: cover formatter output trimming and indentation`.
188
+ 2. **feat: add formatRunOutputBlock helper**
189
+ - Implement `src/formatter-output-report.ts` to make step 1 pass.
190
+ - Commit: `feat: add formatter run output trimming helper`.
191
+ 3. **test: extend AutoformatConfig with formatterOutput defaults**
192
+ - In `test/formatter-config.test.ts`, assert the default object on `AutoformatConfig` and that `createFormatterConfig` merges a partial `formatterOutput` user object field-by-field.
193
+ - Commit: `test: cover formatterOutput config defaults and merge`.
194
+ 4. **feat: thread formatterOutput through config**
195
+ - Add the new types/defaults and merge logic in `src/formatter-config.ts`.
196
+ - Commit: `feat: add formatterOutput config object with safe defaults`.
197
+ 5. **test: validate formatterOutput in schema and loader**
198
+ - In `test/schema.test.ts`, assert the property and reject an unknown sub-key.
199
+ - In `test/config-loader.test.ts`, assert that an invalid `onFailure` produces a `ConfigValidationIssue` and falls back to the default.
200
+ - Commit: `test: lock formatterOutput schema and loader validation`.
201
+ 6. **feat: extend schema with formatterOutput**
202
+ - Update `schemas/pi-autoformat.schema.json`; ensure step 5 passes.
203
+ - Commit: `feat: surface formatterOutput in the JSON schema`.
204
+ 7. **test: enrich failure block under each onFailure value**
205
+ - In `test/extension.test.ts`, drive `defaultReportFlushResult` with a single failed run carrying `stdout` + `stderr` content, under `onFailure: "none"` (block omitted), `"stderr"` (only stderr block), `"both"` (stdout above stderr).
206
+ - Assert empty stdout under `"both"` does not produce an empty header.
207
+ - Assert successful runs are never annotated even with `"both"`.
208
+ - Commit: `test: cover formatter output reporting on failed runs`.
209
+ 8. **feat: include formatter output in failure notifications**
210
+ - Wire `config.formatterOutput` through `summarizeFailures` / `buildLegacyFailureMessage` in `src/extension.ts` to make step 7 pass.
211
+ - Commit: `feat: surface failed formatter output in reports`.
212
+ 9. **test: respect truncation under realistic chatty output**
213
+ - One end-to-end test feeding a multi-kilobyte stderr through the full failure path, asserting the marker appears and the tail is preserved.
214
+ - Commit: `test: lock truncation behavior in the failure report`.
215
+ 10. **docs: document formatterOutput**
216
+ - Update `docs/configuration.md` with the new section and example; update the `README.md` "Known v1 limitations" line to point at the option.
217
+ - Commit: `docs: document the formatterOutput failure reporting option`.
218
+
219
+ ## Risks and Mitigations
220
+
221
+ - **Risk: chatty formatters flood the TUI even when truncated.**
222
+ Mitigation: byte cap runs before line cap; defaults (4 KiB / 40 lines) keep one failure under one screen.
223
+ Caps are configurable for users who need more.
224
+ - **Risk: secrets leak through formatter output (e.g. file contents in error messages).**
225
+ Mitigation: the option is opt-in and off by default; documented as such.
226
+ We do not snapshot output to disk.
227
+ - **Risk: schema additions break existing configs because of `additionalProperties: false`.**
228
+ Mitigation: the new key is optional, defaults preserve current behavior, and existing configs without it remain valid.
229
+ - **Risk: truncation marker confuses users who expect to see the full output.**
230
+ Mitigation: marker phrasing names the byte/line counts dropped; docs explain the cap and how to raise it.
231
+ - **Risk: differing TUI vs non-TUI output drift.**
232
+ Mitigation: both sinks consume the same string built by the same helper; covered by tests.
233
+ - **Risk: formatters that emit useful info on stdout (not stderr) are missed under `"stderr"`.**
234
+ Mitigation: `"both"` covers them; documented in the configuration reference.
235
+
236
+ ## Open Questions
237
+
238
+ - Should the success path eventually expose a "verbose summary" (formatter stdout for successful runs) for debugging "did this formatter actually do anything?" cases?
239
+ Defer; not what the issue asks for.
240
+ - Should we add a separate `onSuccess` knob mirroring `onFailure`?
241
+ Defer until requested; current scope keeps the surface narrow.
242
+ - Should the truncated tail be replaced with the *head* for formatters whose first lines are most informative (e.g. compiler diagnostics that print the error first then a giant context dump)?
243
+ Tail-first matches the dominant case (parser errors print last); revisit if user feedback says otherwise.
244
+ - Should we ever write full output to a per-run log file under `.pi/extensions/pi-autoformat/`?
245
+ Defer; mechanism is forever, this is reversible from docs.