@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,220 @@
1
+ ---
2
+ issue: 1
3
+ issue_title: "Add richer TUI formatter summaries"
4
+ ---
5
+
6
+ # Plan: Richer TUI Formatter Summaries (Issue #1)
7
+
8
+ ## Problem Statement
9
+
10
+ Formatter activity is reported through transient `ctx.ui.notify` toasts.
11
+ Successes pop up and disappear; failures pop up and disappear.
12
+ The user has no ambient indicator that "formatters ran in the background", and a failure that the user would like to revisit later can be missed if a notification is dismissed.
13
+
14
+ The issue asks for richer, more scannable summaries — especially when several files or chains run in one flush — while keeping the surface low-key on the happy path and more prominent (but still non-urgent) on the failure path.
15
+
16
+ ## Goals
17
+
18
+ - Use `ctx.ui.setStatus` to render a persistent, low-prominence summary in the footer/status bar after each prompt-end flush, so the user sees "this happened in the background" without a toast.
19
+ - On failure, keep `ctx.ui.notify(..., "warning")` so the failure catches the eye once, **and** leave an error-colored footer status so the user can come back to it later in the session.
20
+ - Make per-chain and per-file breakdowns more scannable when multiple files or chains run in a single flush.
21
+ - Preserve the existing non-interactive behavior (`console.log` / `console.warn`) unchanged.
22
+ - Preserve the existing `hideSummariesInTui` semantics: when `true`, success summaries are suppressed from the TUI; failures still surface.
23
+
24
+ This change is **not** breaking at the config layer — no new config keys, no schema change.
25
+ It does change the TUI surface used for success summaries (from notify to setStatus), which is a user-visible behavior change but not a config-breaking one.
26
+
27
+ ## Non-Goals
28
+
29
+ - A `setWidget` block above the editor with a full per-file table.
30
+ The user's framing — "enough feedback to say this happened in the background" — does not justify the screen real estate.
31
+ We can revisit if feedback shows the footer is too thin.
32
+ - Adding a `summarySurface` config key (notify | widget | status | none).
33
+ Premature; one good default beats a knob.
34
+ Re-open if multiple users disagree with the chosen surface.
35
+ - Persisting summaries across sessions.
36
+ Status is cleared on `session_shutdown` per Pi conventions.
37
+ - Restructuring `PromptAutoformatterResult` or the executor.
38
+ This plan is purely a reporting-layer change inside `src/extension.ts`.
39
+ - Changing failure prominence beyond keeping today's `notify(warning)` plus the new persistent footer entry.
40
+ - Theme overrides or custom color names beyond the standard `success | warning | error | dim | accent` foreground colors already used by example extensions.
41
+
42
+ ## Background
43
+
44
+ Relevant existing pieces:
45
+
46
+ - `src/extension.ts`
47
+ - `defaultReportFlushResult(result, { config, ctx })` — currently the only sink for formatter summaries.
48
+ Sends multi-line text to `ctx.ui.notify` (success or warning) on TUI, or `console.log` / `console.warn` otherwise.
49
+ - `summarizeFailures`, `summarizeFallbackUsages`, `collectAllFiles`, `summarizeSuccessPaths`, `formatterLabel` — pure helpers that turn a `PromptAutoformatterResult` into lines.
50
+ These are reusable as-is.
51
+ - `reportMessage(ctx, message, type)` — routes to `notify` when `ctx.hasUI`, otherwise to `console.warn`/`console.log`.
52
+ - `src/formatter-config.ts` — `hideSummariesInTui: boolean` (default `false`).
53
+ Currently consulted only for the success branch in `defaultReportFlushResult`.
54
+ - Pi extension API surface (from `pi-mono/packages/coding-agent/src/core/extensions/types.ts`):
55
+ - `ctx.ui.notify(message, type?)` — transient toast.
56
+ - `ctx.ui.setStatus(key, text | undefined)` — persistent footer/status text; pass `undefined` to clear.
57
+ - `ctx.ui.theme.fg("success" | "warning" | "error" | "dim" | "accent", text)` — themed foreground color, used by the `status-line.ts` example extension.
58
+ - `setWidget` and `custom` exist but are out of scope (see Non-Goals).
59
+ - Lifecycle:
60
+ `session_start` → many `tool_call`/`tool_result` pairs → `agent_end` (the prompt-end flush we report on) → eventually `session_shutdown`.
61
+ Multiple flushes happen per session in interactive use.
62
+
63
+ ## Design Overview
64
+
65
+ ### Reporting flow
66
+
67
+ 1. Per flush, `defaultReportFlushResult` builds two views from the existing helpers:
68
+ - **success view** — chain count, file count, fallback-usage suffix.
69
+ - **failure view** — failed batch count and per-batch lines.
70
+ 2. On TUI (`ctx.hasUI === true`):
71
+ - If the result has zero groups, clear the status (`setStatus("autoformat", undefined)`) and return.
72
+ A no-op flush should not leave a stale "Autoformatted 3 files" line in the footer.
73
+ - If only successes:
74
+ - When `hideSummariesInTui` is `true`, clear the status and return.
75
+ - Otherwise call `setStatus("autoformat", themedSuccessLine)`.
76
+ Do **not** call `notify`.
77
+ - If any failures:
78
+ - Call `setStatus("autoformat", themedFailureLine)` so the user can come back to it.
79
+ - Call `notify(failureBlock, "warning")` once with the existing multi-line block (failed batch count + per-batch lines), so the failure catches the eye.
80
+ 3. On non-TUI (`ctx.hasUI === false`):
81
+ - Keep today's `console.log` / `console.warn` output verbatim.
82
+ - Do not call `setStatus` (it's a no-op without a UI but adds noise to mocks).
83
+
84
+ ### Status text format
85
+
86
+ Single line, themed, ASCII-only by default to stay readable in minimal terminals.
87
+ Dim parens carry low-priority detail.
88
+
89
+ ```text
90
+ ✓ autoformat: 3 files (biome, prettier)
91
+ ✓ autoformat: 1 file (biome)
92
+ ✗ autoformat: 1 batch failed (biome) — 2 ok
93
+ ```
94
+
95
+ - `✓` rendered via `theme.fg("success", "✓")`; the "autoformat:" label via `theme.fg("dim", ...)`.
96
+ - `✗` rendered via `theme.fg("error", "✗")`; the failure clause via `theme.fg("error", ...)`.
97
+ - Formatter names in the success line are deduplicated and ordered by first appearance across `result.groups[].runs[].formatterName`.
98
+ - Fallback usages append `(fallback after X)` only when present, reusing `formatterLabel`.
99
+ - The line is intentionally short (~one terminal row).
100
+ Per-file breakdown stays in the failure notify block, which already lists files per failed batch.
101
+
102
+ ### Lifecycle hooks
103
+
104
+ - `session_start`: clear `setStatus("autoformat", undefined)` so a fresh session does not inherit a stale status from a previous run rendered in the same UI.
105
+ - `session_shutdown`: clear status before tearing the session down, alongside the existing `unsubscribeEventBus`.
106
+
107
+ These are small additions to existing handlers in `createAutoformatExtension`.
108
+
109
+ ### Status key
110
+
111
+ Use `"autoformat"` as the `setStatus` key.
112
+ Single key per extension, reused across flushes (each call replaces the previous value).
113
+ Documented in `docs/configuration.md` as informational so users writing custom themes know it exists.
114
+
115
+ ### Types
116
+
117
+ No public type changes.
118
+ Internally, introduce a small helper so the TUI/non-TUI branches share one source of truth:
119
+
120
+ ```typescript
121
+ type FlushSummary = {
122
+ groupCount: number;
123
+ fileCount: number;
124
+ formatterNames: string[];
125
+ failureBatchCount: number;
126
+ failureLines: string[];
127
+ fallbackUsages: string[];
128
+ };
129
+
130
+ function summarizeFlush(result: PromptAutoformatterResult): FlushSummary;
131
+ ```
132
+
133
+ `defaultReportFlushResult` becomes a thin dispatcher that builds a `FlushSummary` once, then renders for TUI vs non-TUI.
134
+
135
+ ### Edge cases
136
+
137
+ - **Empty flush** (`result.groups.length === 0`): clear status; emit nothing on non-TUI (today's behavior).
138
+ - **All-fallback-skipped chains** are already filtered upstream (`flushPrompt` drops empty groups), so they cannot produce a phantom status line.
139
+ - **Mixed success and failure**: the status reflects failure (error-colored), success counts shown after an em dash; notify fires for failures only.
140
+ - **Theme without color** / non-color terminals: `theme.fg` is theme-aware and the example extensions rely on it; we trust it to degrade.
141
+ - **No `theme` available**: defensively, fall back to plain text if `ctx.ui.theme` is undefined (matches our existing duck-typed `ExtensionContextLike`).
142
+ - **Repeated flushes**: each call to `setStatus("autoformat", ...)` replaces the previous value, so the footer always reflects the latest flush.
143
+
144
+ ## Module-Level Changes
145
+
146
+ - `src/extension.ts`
147
+ - Extend `ExtensionContextLike.ui` to include optional `setStatus(key: string, text: string | undefined): void` and an optional `theme` shape with `fg(name: string, text: string): string`.
148
+ Both optional so existing tests using minimal stub UIs do not need to change unless they assert on status behavior.
149
+ - Add `summarizeFlush(result)` helper consolidating today's per-piece helpers.
150
+ - Add `formatStatusLine(summary, { theme })` helper returning a single string.
151
+ - Rewrite `defaultReportFlushResult` to:
152
+ 1. Compute `FlushSummary`.
153
+ 2. Branch on `ctx.hasUI`.
154
+ 3. On TUI: empty → clear; success → `setStatus`; failure → `setStatus` + `notify`.
155
+ 4. On non-TUI: existing `console.log` / `console.warn` paths, unchanged.
156
+ - In `createAutoformatExtension`:
157
+ - On `session_start`, after `reportConfigIssues`, call `clearAutoformatStatus(ctx)`.
158
+ - On `session_shutdown`, before `state = undefined`, call `clearAutoformatStatus(ctx)`.
159
+ - `clearAutoformatStatus` is a 3-line helper guarded by `ctx.hasUI` and `ctx.ui.setStatus`.
160
+ - `test/extension.test.ts`
161
+ - New tests for the rewritten `defaultReportFlushResult` (see TDD Order).
162
+ - Existing notify-based assertions for success summaries change to assert on `setStatus`; failure-path assertions still expect `notify(warning)` and additionally assert `setStatus` with error styling.
163
+ - `docs/configuration.md`
164
+ - Update the `hideSummariesInTui` section to reflect the new surface ("suppresses the persistent footer status on success; failures still surface via notification + status").
165
+ - `README.md`
166
+ - One-paragraph update to the formatter-feedback section: footer status on success, notification + status on failure.
167
+ - No changes to:
168
+ - `src/config-loader.ts`, `src/formatter-config.ts`, `schemas/pi-autoformat.schema.json` — no config additions.
169
+ - `src/prompt-autoformatter.ts`, `src/formatter-executor.ts` — reporting-layer-only change.
170
+
171
+ ## TDD Order
172
+
173
+ 1. **test: cover empty-flush status clearing**
174
+ - In `test/extension.test.ts`, add a test that calls `defaultReportFlushResult` with `{ groups: [] }` on a TUI ctx and asserts `setStatus("autoformat", undefined)` is called and `notify` is not.
175
+ - Commit: `test: cover empty-flush autoformat status clearing`.
176
+ 2. **feat: route success summaries through setStatus**
177
+ - Implement `summarizeFlush`, `formatStatusLine`, and the success branch of the new `defaultReportFlushResult` (TUI path) so the empty-flush test plus a new "single chain, multi-file success" test pass.
178
+ - Assert `setStatus` is called with a string containing "autoformat" and the file count; assert `notify` is **not** called.
179
+ - Commit: `feat: render formatter success summaries in the footer status`.
180
+ 3. **feat: surface failures via setStatus and notify together**
181
+ - Add a "one failed batch + one successful batch" test asserting both `setStatus` (error styling) and `notify(..., "warning")` are called, and the notify body still contains the per-batch failure lines.
182
+ - Commit: `feat: keep failure notifications and add persistent failure status`.
183
+ 4. **feat: honor hideSummariesInTui for the new status surface**
184
+ - Add tests covering `hideSummariesInTui: true` for success-only (clears status, no notify) and for failure (status + notify still fire).
185
+ - Commit: `feat: respect hideSummariesInTui for footer status`.
186
+ 5. **feat: clear autoformat status on session_start and session_shutdown**
187
+ - Add lifecycle tests that drive the extension through `session_start` and `session_shutdown` and assert `setStatus("autoformat", undefined)` is invoked at each.
188
+ - Commit: `feat: clear autoformat status on session lifecycle boundaries`.
189
+ 6. **test: preserve non-interactive console output**
190
+ - Add or update tests that assert non-TUI flushes still go through `console.log` / `console.warn` and never call `setStatus`.
191
+ - Commit: `test: lock in non-interactive autoformat reporting behavior`.
192
+ 7. **docs: align configuration and README with the new surface**
193
+ - Update `docs/configuration.md` (`hideSummariesInTui` section) and `README.md`.
194
+ - Commit: `docs: describe footer-status formatter summaries`.
195
+
196
+ ## Risks and Mitigations
197
+
198
+ - **Risk: status surface surprises users who relied on success toasts.**
199
+ Mitigation: failures still toast; success toasts were already opt-out via `hideSummariesInTui`, and the issue explicitly flags small-success notifications as noise.
200
+ Document the change in `docs/configuration.md` and `README.md` in step 7.
201
+ - **Risk: stale status from a previous flush on session reuse.**
202
+ Mitigation: clear on `session_start` and `session_shutdown`; each new flush overwrites the same `"autoformat"` key.
203
+ - **Risk: `setStatus` not present on older Pi runtimes.**
204
+ Mitigation: feature-detect (`typeof ctx.ui.setStatus === "function"`) before calling; fall back to today's `notify(info)` if unavailable.
205
+ This is a small `if` branch, not a new mechanism.
206
+ - **Risk: theme color not available.**
207
+ Mitigation: feature-detect `ctx.ui.theme?.fg`; fall back to plain `"✓ autoformat: …"` text.
208
+ - **Risk: footer line clashes with other extensions writing to the footer.**
209
+ Mitigation: use a distinct key (`"autoformat"`); Pi's `setStatus` is keyed precisely so multiple extensions can coexist.
210
+ - **Risk: confusing behavior when `hideSummariesInTui` was understood as "no toasts".**
211
+ Mitigation: docs update clarifies it now means "no success summary in TUI, regardless of surface".
212
+
213
+ ## Open Questions
214
+
215
+ - Should we add an opt-in `setWidget` block (above editor) for users who want the per-file breakdown ambient instead of buried in a notify body?
216
+ Defer until someone asks for it.
217
+ - Should `hideSummariesInTui` be renamed to `quietSuccess` or similar now that the surface changed?
218
+ Defer; keep the name and update its doc.
219
+ - Should non-interactive mode also become quieter (e.g. drop success lines)?
220
+ Out of scope; the issue is specifically about the TUI.
@@ -0,0 +1,273 @@
1
+ ---
2
+ issue: 3
3
+ issue_title: "Post-v1: support additional Pi mutation tools"
4
+ ---
5
+
6
+ # Plan: Additional Pi Mutation Tools (Issue #3)
7
+
8
+ ## Problem Statement
9
+
10
+ The v1 extension only enqueues touched files for two tool names: Pi's built-in `write` and `edit`.
11
+ Any other tool that mutates files — whether provided by another Pi extension, an MCP server, or a future built-in — flows through the `tool_result` event without being recognized, so its outputs are never formatted.
12
+
13
+ Issue #3 asks us to "support additional Pi mutation tools" without broadening to heuristic file scanning, and to "design a clean touched-file reporting interface for custom tools."
14
+
15
+ ## Goals
16
+
17
+ - Recognize mutations performed by **non-built-in** tools (extension- or MCP-provided) and feed the resulting paths into the existing prompt-end batching pipeline.
18
+ - Provide a low-cost integration path for other extensions: declarative config for the common case, and a small event-bus contract for the dynamic case.
19
+ - Keep behavior explicit, opt-in, and predictable.
20
+ No inference, no whole-repo scans.
21
+ - Reuse the existing `MutationSourceHandler` plumbing, scope filtering, dedupe, and reporting paths unchanged.
22
+
23
+ ## Non-Goals
24
+
25
+ - Tracking shell-driven mutations.
26
+ That is Issue #4 and has its own plan (`0004-shell-driven-mutation-coverage.md`).
27
+ The two efforts share the same `TouchedFilesQueue` but are independent.
28
+ - Adding a stable, versioned public TypeScript API for cross-extension use.
29
+ We expose only the existing `pi.events` channel; everything else stays internal.
30
+ - Inferring mutation intent from tool names, schemas, or output content.
31
+ All recognition is opt-in and explicit.
32
+ - Strict-mode failure semantics (Issue #6).
33
+
34
+ ## Background
35
+
36
+ What we know from `pi-mono` (verified against `packages/coding-agent/src/core/extensions/types.ts` and `event-bus.ts` at the current `main`):
37
+
38
+ - The complete set of **built-in** Pi tools is `bash | read | edit | write | grep | find | ls`.
39
+ Of those, only `bash`, `edit`, and `write` mutate files. `edit` and `write` are already covered; `bash` is reserved for Issue #4. **There are no additional built-in mutation tools to wire up.**
40
+ - Extensions can register arbitrary tools via `pi.registerTool()`.
41
+ Their results arrive as `CustomToolResultEvent` through the same `tool_result` event we already subscribe to, with the shape:
42
+
43
+ ```ts
44
+ {
45
+ type: "tool_result";
46
+ toolName: string; // arbitrary
47
+ input: Record<string, unknown>; // tool's args
48
+ details: unknown; // tool-specific payload
49
+ content: (TextContent | ImageContent)[];
50
+ isError: boolean;
51
+ }
52
+ ```
53
+
54
+ So extension-provided mutation tools already flow past our `tool_result` handler — we just don't recognize their names or know which input field carries the path(s).
55
+ - Pi exposes a shared `pi.events: EventBus` with a stable `emit(channel, data)` / `on(channel, handler)` shape.
56
+ This means we can subscribe to a documented channel without coordinating any new public API on Pi's side.
57
+
58
+ The extension architecture is already compatible with new mutation sources — `TouchedFilesQueue` accepts a list of `MutationSourceHandler`s and applies scope filtering and dedupe centrally.
59
+ The work is in *declaration* and *subscription*.
60
+
61
+ Relevant existing pieces:
62
+
63
+ - `src/touched-files-queue.ts` — `MutationSourceHandler` registry, central path normalization and scope filtering.
64
+ - `src/extension.ts` — assembles the handler list inside `createDefaultAutoformatter` and wires the `tool_result` event.
65
+ - `src/formatter-config.ts` / `src/config-loader.ts` — config shape and precedence.
66
+ - `src/shell-mutation-detector.ts` — example of a config-driven handler factory; the new work mirrors its structure.
67
+
68
+ ## Design Overview
69
+
70
+ Two complementary, independent mutation sources.
71
+ Either can be used without the other.
72
+ Both feed the existing `TouchedFilesQueue` through new `MutationSourceHandler`s, so scope filtering, dedupe, and prompt-end batching are unchanged.
73
+
74
+ ### Source 1: Config-declared custom tools (default off, primary path)
75
+
76
+ Users declare which non-built-in tools mutate files and where the path(s) live in the `input` payload:
77
+
78
+ ```jsonc
79
+ {
80
+ "customMutationTools": [
81
+ { "toolName": "mcp_files_write", "pathField": "path" },
82
+ { "toolName": "mcp_files_move", "pathField": "destination" },
83
+ { "toolName": "codemod_apply", "pathFields": ["target", "extraTargets"] }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ Field semantics:
89
+
90
+ - `toolName` (string, required): exact match against `event.toolName`.
91
+ - `pathField` (string, optional): single dotted path into `event.input`.
92
+ Resolves nested fields, e.g. `"args.path"`.
93
+ - `pathFields` (string[], optional): multiple dotted paths.
94
+ - For both forms, the resolved value may be a string or a string array; string arrays are flattened.
95
+ Non-string scalars (numbers, booleans, null) are ignored. `pathField` and `pathFields` differ only in arity, not in value handling — a tool whose field is sometimes a string and sometimes a string array should not require switching keys. *(Refined during test-first implementation: the original draft made `pathField` string-only, which would have forced users to switch to `pathFields` for any tool with a variadic field.
96
+ Unifying value handling removes that foot-gun.)*
97
+ - Exactly one of `pathField` / `pathFields` is required.
98
+ Specifying both is a config validation error.
99
+
100
+ Recognition rules:
101
+
102
+ - Only act on `tool_result` events whose `isError === false`.
103
+ - Built-in tool names (`bash`, `edit`, `write`, `read`, `grep`, `find`, `ls`) are rejected at config load with a validation issue. `edit` and `write` are already covered; the others are not mutating tools and declaring them would be a configuration mistake.
104
+ - Duplicate `toolName` entries are an error at config load.
105
+ - Per-handler output is fed into the queue, which applies the standard `formatScope` filter and dedupe.
106
+ No new path-handling logic is added.
107
+
108
+ This covers the common, declarative case (MCP file servers, simple codemod tools, "rename" tools, etc.) without code changes.
109
+
110
+ ### Source 2: EventBus channel (opt-in, dynamic case)
111
+
112
+ For tools that compute touched paths dynamically — codegen that emits N files, batch operations, tools whose `input` does not contain the written paths — extensions can emit on a documented channel:
113
+
114
+ ```ts
115
+ // In another extension:
116
+ pi.events.emit("autoformat:touched", { path: "src/generated/api.ts" });
117
+ // or:
118
+ pi.events.emit("autoformat:touched", {
119
+ paths: ["src/a.ts", "src/b.ts"]
120
+ });
121
+ ```
122
+
123
+ Channel contract:
124
+
125
+ - Channel name: `autoformat:touched`.
126
+ - Payload: `{ path: string }` or `{ paths: string[] }`.
127
+ Other shapes are ignored silently (no warnings — this channel is best-effort and we must not log on every emission from misconfigured peers).
128
+ - Paths follow the same normalization and scope rules as every other source.
129
+ Out-of-scope paths are dropped silently.
130
+ - Emissions are accepted at any time during a session; queued paths are flushed at the next prompt-end (consistent with the default timing model).
131
+
132
+ Subscribing is feature-flagged in config (default on once the feature ships) so users can disable it if a peer extension misbehaves:
133
+
134
+ ```jsonc
135
+ {
136
+ "eventBusMutationChannel": {
137
+ "enabled": true,
138
+ "channel": "autoformat:touched"
139
+ }
140
+ }
141
+ ```
142
+
143
+ Defaulting to *on* is acceptable here because the channel is a no-op unless a peer extension actually emits on it.
144
+ The `channel` override exists for testing and for the rare case of a name collision.
145
+
146
+ ## Configuration
147
+
148
+ New top-level keys, both extension-owned (per AGENTS.md):
149
+
150
+ ```jsonc
151
+ {
152
+ "customMutationTools": [],
153
+ "eventBusMutationChannel": {
154
+ "enabled": true,
155
+ "channel": "autoformat:touched"
156
+ }
157
+ }
158
+ ```
159
+
160
+ Precedence: project overrides global (existing behavior).
161
+ For `customMutationTools`, project replaces global wholesale (consistent with how arrays are currently merged elsewhere — confirm and document).
162
+
163
+ Aligned updates required (per AGENTS.md):
164
+
165
+ - `schemas/pi-autoformat.schema.json`
166
+ - `docs/configuration.md`
167
+ - `README.md`
168
+ - TypeScript config types in `src/formatter-config.ts`
169
+ - Loader and validation in `src/config-loader.ts`
170
+
171
+ ## Code Changes
172
+
173
+ 1. **Config**
174
+ - Extend `UserFormatterConfig` and `AutoformatConfig` in `src/formatter-config.ts` with `customMutationTools` and `eventBusMutationChannel`.
175
+ - Defaults: empty array, channel enabled with the documented name.
176
+ - Loader validation in `src/config-loader.ts`:
177
+ - Reject built-in tool names.
178
+ - Reject duplicate `toolName` entries.
179
+ - Require exactly one of `pathField` / `pathFields`.
180
+ - Validate dotted-path strings are non-empty.
181
+
182
+ 2. **New module: `src/custom-mutation-tools.ts`**
183
+ - Pure function `extractPathsFromInput(input, fieldSpec)` that resolves dotted paths and flattens string arrays.
184
+ - Factory `createCustomToolHandler(spec): MutationSourceHandler`.
185
+ - Factory `createCustomToolHandlers(specs[]): MutationSourceHandler[]`.
186
+ - No I/O; fully unit-testable.
187
+
188
+ 3. **Extension wiring (`src/extension.ts`)**
189
+ - In `createDefaultAutoformatter`, append handlers from `createCustomToolHandlers(config.customMutationTools)` to the existing handler list, after `writeOrEditHandler` and before the bash detector.
190
+ - Order is irrelevant for correctness (queue dedupes) but stable ordering keeps tests deterministic.
191
+
192
+ 4. **EventBus subscription**
193
+ - Accept the `EventBus` (or a small adapter) through `ExtensionApiLike` so it can be stubbed in tests.
194
+ Today `ExtensionApiLike` only exposes `on` for the lifecycle events; we add an optional `events` field and tolerate its absence.
195
+ - At session start, if `eventBusMutationChannel.enabled`, subscribe to the configured channel and forward valid payloads via `autoformatter.addTouchedPath(...)`.
196
+ Unsubscribe on `session_shutdown`.
197
+ - Payload validation lives in a small pure helper for testing (`parseTouchedPayload(unknown): string[]`).
198
+
199
+ 5. **No changes** to `formatter-executor`, `prompt-autoformatter`, or reporting.
200
+ Both new sources surface through the existing prompt-end summary.
201
+
202
+ ## Testing
203
+
204
+ Per AGENTS.md, focused tests:
205
+
206
+ - `extractPathsFromInput`
207
+ - top-level string field → `[value]`
208
+ - nested dotted path resolves
209
+ - missing field → `[]`
210
+ - non-string value → `[]` (no coercion)
211
+ - `pathFields` with a string-array value flattens
212
+ - `pathFields` with mixed string + string-array entries flatten
213
+ - `createCustomToolHandler`
214
+ - matching `toolName` produces paths
215
+ - non-matching `toolName` produces `[]`
216
+ - errored tool result is not handled (handler is wired only to successful results in the extension layer; assert at the wiring level)
217
+ - Config loader
218
+ - rejects built-in tool names with a clear validation issue
219
+ - rejects duplicate `toolName`
220
+ - rejects entries with both / neither of `pathField`/`pathFields`
221
+ - rejects empty / non-string dotted paths
222
+ - project `customMutationTools` replaces global (does not merge)
223
+ - defaults: empty array, channel enabled
224
+ - `parseTouchedPayload`
225
+ - `{ path: "x" }` → `["x"]`
226
+ - `{ paths: ["a", "b"] }` → `["a", "b"]`
227
+ - `{ paths: ["a", 1, null] }` → `["a"]` (drops non-strings)
228
+ - unknown shape → `[]`
229
+ - non-object → `[]`
230
+ - Extension integration (`extension.test.ts` style)
231
+ - declared custom tool's `tool_result` event populates touched files and triggers prompt-end formatting
232
+ - paths outside `formatScope` are dropped (delegated to existing queue behavior; one regression test is enough)
233
+ - EventBus emission feeds the queue and survives across multiple prompts
234
+ - disabling `eventBusMutationChannel.enabled` prevents subscription
235
+ - missing `pi.events` does not throw (graceful degrade)
236
+
237
+ ## Rollout
238
+
239
+ 1. Land config schema + loader validation with empty defaults; no behavior change.
240
+ 2. Land `extractPathsFromInput` and `createCustomToolHandler` with unit tests.
241
+ 3. Wire custom-tool handlers into `createDefaultAutoformatter`.
242
+ 4. Add EventBus subscription, gated on the new config flag and on `pi.events` being present.
243
+ 5. Documentation pass: `docs/configuration.md`, `README.md`, schema, and a short "integration guide for other extensions" snippet covering both sources.
244
+ 6. Update `docs/plans/0001-initial-implementation-plan.md` to reference the new mutation sources.
245
+
246
+ ## Open Questions
247
+
248
+ - Do we want to expose **details-based** extraction in addition to `input`-based (e.g., `detailsField`)? Some custom tools may report written paths in `event.details` rather than `event.input`.
249
+ Likely yes, but the schema would mirror `pathField`/`pathFields`.
250
+ Defer unless a real consumer needs it; easy to add later.
251
+ - Should the EventBus payload also accept `{ paths: string }` (singular in the plural key)? Probably no — keep the contract strict and documented.
252
+ - Should we provide a tiny "testing helper" module that other extensions can import to construct valid payloads? Out of scope for this issue; the contract is small enough to inline.
253
+
254
+ ## Explicitly Deferred
255
+
256
+ - **Programmatic registration API** (e.g., `pi-autoformat.registerMutationSource(handler)`).
257
+ The EventBus channel covers the dynamic case without locking us into a stable TypeScript API surface.
258
+ We can add a typed wrapper module later if real-world usage shows it is needed.
259
+ - **Tool-name pattern matching** (regex / glob over `toolName`).
260
+ The current MCP naming conventions are stable enough that exact match is sufficient.
261
+ Patterns can be added without breaking the existing schema.
262
+ - **Auto-discovery from tool schemas.** Several MCP tools advertise a `path: string` parameter, and we could heuristically opt them in.
263
+ This is exactly the "implicit, surprising" behavior AGENTS.md warns against; keep declarations explicit.
264
+
265
+ ## Checkpoints / Commits
266
+
267
+ Following Conventional Commits:
268
+
269
+ - `feat(config): add customMutationTools and eventBusMutationChannel schema`
270
+ - `feat: extract touched paths from declared custom tool inputs`
271
+ - `feat: subscribe to autoformat:touched event-bus channel`
272
+ - `docs: document custom mutation tool integration`
273
+ - `test: cover custom mutation tool handlers and event-bus subscription`