@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,201 @@
1
+ ---
2
+ issue: 22
3
+ issue_title: "Depend on @mariozechner/pi-coding-agent for runtime types instead of duck-typing"
4
+ ---
5
+
6
+ # Plan: Adopt `@mariozechner/pi-coding-agent` Types (Issue #22)
7
+
8
+ ## Problem Statement
9
+
10
+ `src/extension.ts` declares the entire Pi runtime API surface as duck-typed `*Like` aliases (`ExtensionApiLike`, `ExtensionContextLike`, `ExtensionUILike`, `ToolResultEventLike`, `ToolCallEventLike`, `TextContentLike`, `ThemeColorName`).
11
+ Pi already publishes the real shapes via `@mariozechner/pi-coding-agent`, but we have no Pi dependency in `package.json`, so the duck-typed surface is what TypeScript checks against.
12
+
13
+ That looseness shipped a real bug: our `theme?: { fg(...): string }` shape happily accepted plain arrow-function stubs in tests, which hid the `this`-binding regression in `themed()` for an entire release cycle until a user surfaced it (retro `0016`, fixes `6a6ec16` / `6ba7576`).
14
+ Anchoring against Pi's real types — especially the `Theme` class — would have caught the bug at compile time.
15
+
16
+ This is a typing-only refactor: no behavior change, no public-config change, no schema change.
17
+
18
+ ## Goals
19
+
20
+ - Add `@mariozechner/pi-coding-agent@^0.72.0` to `devDependencies` (types-only; Pi loads us at runtime).
21
+ - Replace every `*Like` alias in `src/extension.ts` with the corresponding real export from `@mariozechner/pi-coding-agent`.
22
+ - Type the public entrypoint and `pi.on(...)` handlers with the real `ExtensionAPI`, `ExtensionContext`, `ToolCallEvent`, and `ToolResultEvent` so a class-based `Theme` substitute is required at the boundary.
23
+ - Update test stubs (`test/extension.test.ts`, others as needed) so they compile against the real types and continue to exercise the behavior we already cover.
24
+ - Keep the diff zero-runtime-weight: no `dependencies`, no new imports of values from `@mariozechner/pi-coding-agent` (types only).
25
+
26
+ This change is **not** breaking from the consumer's perspective — Pi already injects these shapes at runtime.
27
+ It is, however, a build-time tightening: a future incompatible Pi release will fail our build until the dep is bumped.
28
+
29
+ ## Non-Goals
30
+
31
+ - Adding `@mariozechner/pi-coding-agent` to `dependencies` or `peerDependencies`.
32
+ Pi is the loader, not a consumer of our package.
33
+ - Behavior changes to formatter dispatch, status reporting, or config loading.
34
+ - Touching `src/config-loader.ts`, `src/formatter-config.ts`, `src/prompt-autoformatter.ts`, etc. — none of them reference the Pi runtime API.
35
+ - Pinning Pi tighter than the issue suggests; `^0.72.0` stays.
36
+ - Updating `schemas/pi-autoformat.schema.json`, `docs/configuration.md`, `README.md`, or `docs/plans/`.
37
+ None reference the duck-typed shapes.
38
+ - Adopting Pi's `isBashToolResult` / `isEditToolResult` / `isWriteToolResult` type guards as the dispatch mechanism.
39
+ Our existing `toolName === "bash"` / `"edit"` / `"write"` string checks are equivalent and out of scope; introducing new dispatch utilities is a separate change.
40
+
41
+ ## Background
42
+
43
+ Relevant pieces in this repo:
44
+
45
+ - `src/extension.ts` declares (around lines 35–100):
46
+ - `NotificationType` — kept (it's our own narrow alias for the literal union, not duck-typing).
47
+ - `ThemeColorName` — superseded by `ThemeColor` (re-exported from Pi's main entrypoint).
48
+ - `ExtensionUILike` — superseded by `ExtensionUIContext`.
49
+ - `ExtensionContextLike` — superseded by `ExtensionContext`.
50
+ - `TextContentLike` — Pi exports `TextContent` (and `ToolResultEventBase.content` is `(TextContent | ImageContent)[]`).
51
+ - `ToolCallEventLike` / `ToolResultEventLike` — Pi exports `ToolCallEvent` / `ToolResultEvent` as discriminated unions over `toolName`.
52
+ - `ExtensionApiLike` — Pi exports `ExtensionAPI`. Note Pi's `events` channel is **not** part of `ExtensionAPI` today; we currently piggyback via an optional `events?` property.
53
+ - `src/extension.ts` uses internal helpers (`setAutoformatStatus`, `reportMessage`, `formatStatusLine`, `defaultReportFlushResult`, `subscribeToEventBus`, `runFormatter`, …) that read only `ctx.cwd`, `ctx.hasUI`, `ctx.ui.notify`, `ctx.ui.setStatus`, and `ctx.ui.theme`.
54
+ - `test/extension.test.ts` builds lightweight `TestContext` objects with exactly those fields plus a `theme.fg` stub.
55
+ Real `ExtensionContext` requires `sessionManager`, `modelRegistry`, `model`, `signal`, `isIdle()`, `abort()`, `hasPendingMessages()`, `shutdown()`, `getContextUsage()`, `compact()`, `getSystemPrompt()` — none of which our extension touches.
56
+ - `test/extension.test.ts` already has one `class StubTheme` (the regression test for `6a6ec16`) that demonstrates the class-based stub pattern.
57
+
58
+ Pi's exports we will pull in (all `import type`, all from the main `@mariozechner/pi-coding-agent` entrypoint):
59
+
60
+ - `ExtensionAPI`
61
+ - `ExtensionContext`
62
+ - `ExtensionUIContext`
63
+ - `ToolCallEvent`
64
+ - `ToolResultEvent`
65
+ - `TextContent`
66
+ - `Theme`, `ThemeColor`
67
+
68
+ ## Design Overview
69
+
70
+ ### Boundary policy (decided via `ask-user`)
71
+
72
+ The public entrypoint `autoformatExtension(pi: ExtensionAPI)` and every `pi.on(...)` handler use **real** Pi types.
73
+ That alone is enough to force test stubs to substitute a real `Theme` (or class-based equivalent) for `ctx.ui.theme`, which is the property that hid the regression.
74
+
75
+ Internal helpers narrow their `ctx` parameter to the subset they actually consume:
76
+
77
+ ```typescript
78
+ type AutoformatExtensionContext = Pick<ExtensionContext, "cwd" | "hasUI" | "ui">;
79
+ ```
80
+
81
+ This keeps test setup cheap (no need to fabricate `sessionManager`, `modelRegistry`, etc.) while still anchoring `ui` to Pi's real `ExtensionUIContext`.
82
+ A handler registered through `pi.on("agent_end", handler)` receives a full `ExtensionContext`; passing it to `setAutoformatStatus(ctx)` is a structural-narrowing pass-through, not a cast.
83
+
84
+ ### Event-bus channel
85
+
86
+ Our optional `pi.events?.on(...)` access is **not** part of Pi's `ExtensionAPI` today.
87
+ We preserve the runtime feature by extending the locally-imported `ExtensionAPI` with an optional `events` member through intersection at the `subscribeToEventBus` call site, not by re-declaring the whole API:
88
+
89
+ ```typescript
90
+ type ExtensionAPIWithEvents = ExtensionAPI & {
91
+ events?: {
92
+ on(channel: string, handler: (data: unknown) => void): () => void;
93
+ };
94
+ };
95
+ ```
96
+
97
+ This is the only documented place where we widen Pi's surface.
98
+ If/when Pi adds `events` to `ExtensionAPI` proper, deleting this alias is a one-line change.
99
+
100
+ ### Tool-event handling
101
+
102
+ `ToolResultEvent` is a discriminated union (`BashToolResultEvent | EditToolResultEvent | WriteToolResultEvent | …`).
103
+ Our existing dispatch already keys on `event.toolName === "bash" | "edit" | "write" | "custom_*"`, which TypeScript narrows naturally — no new type guards required.
104
+ `event.content` is typed `(TextContent | ImageContent)[]`; `extractToolOutputText` already filters by `typeof item.text === "string"`, which works unchanged for both branches.
105
+
106
+ ### Test stubs
107
+
108
+ `test/extension.test.ts` `TestContext` and `createContext` change:
109
+
110
+ - `TestContext` becomes `Pick<ExtensionContext, "cwd" | "hasUI"> & { ui: Pick<ExtensionUIContext, "notify" | "setStatus"> & { theme?: Theme } }` (minimal surface required by the helpers under test).
111
+ - `createContext`'s default `theme` becomes a single shared class-based stub (`StubTheme` from the existing regression test, hoisted to a test util) so plain-arrow-function themes are not accepted anywhere.
112
+ - `TestPi.on` is typed as `ExtensionAPI["on"]`; callers narrow per event name.
113
+ - `TestPi.events` is typed against the local `ExtensionAPIWithEvents["events"]`.
114
+
115
+ ### Edge cases
116
+
117
+ - **`ctx.ui.theme` is optional in Pi's types.** All call sites already null-check it; no behavior change.
118
+ - **Tests that emit synthetic tool events** currently build object literals matching `ToolResultEventLike`. Real `ToolResultEvent`'s discriminated branches require concrete `details` and full `content` typing.
119
+ We satisfy them with `as ToolResultEvent` casts at the test boundary (the events do not originate from real Pi at runtime in tests, and the cast is one-place per emit), or — preferred — `satisfies` checks against `Partial<BashToolResultEvent>` etc. where the field set is already complete.
120
+ Choose the lighter option per call site; the goal is "tests still compile and still cover the same paths."
121
+
122
+ ## Module-Level Changes
123
+
124
+ ### `package.json`
125
+
126
+ - Add `"@mariozechner/pi-coding-agent": "^0.72.0"` to `devDependencies`.
127
+ - No other field changes.
128
+
129
+ ### `src/extension.ts`
130
+
131
+ - Remove `ThemeColorName`, `ExtensionUILike`, `ExtensionContextLike`, `TextContentLike`, `ToolResultEventLike`, `ToolCallEventLike` (~25 lines).
132
+ - Add `import type { ExtensionAPI, ExtensionContext, ExtensionUIContext, ToolCallEvent, ToolResultEvent, Theme, ThemeColor } from "@mariozechner/pi-coding-agent";` (only the symbols actually referenced).
133
+ - Introduce local narrow aliases:
134
+ - `type AutoformatExtensionContext = Pick<ExtensionContext, "cwd" | "hasUI" | "ui">;`
135
+ - `type ExtensionAPIWithEvents = ExtensionAPI & { events?: { on(channel: string, handler: (data: unknown) => void): () => void } };`
136
+ - Replace `ExtensionApiLike` with `ExtensionAPI` at the public entrypoint, and `ExtensionAPIWithEvents` only at `subscribeToEventBus`.
137
+ - Replace `ExtensionContextLike` with `AutoformatExtensionContext` on internal helpers; the top-level `pi.on(...)` handlers receive the real `ExtensionContext`.
138
+ - Replace `ThemeColorName` parameter types with `ThemeColor`.
139
+ - Re-export `ExtensionApiLike` is removed (`export type { ExtensionApiLike }`); replace with `export type { ExtensionAPI }` re-export from Pi if any consumer needs it. (Internal-only today; verify with a `rg` before deleting.)
140
+
141
+ ### `test/extension.test.ts`
142
+
143
+ - Drop the local `Handler`, `EventName`, `TestContext` definitions in favor of:
144
+ - `EventName` keyed on Pi's `ExtensionEvent` literal subset we use.
145
+ - `TestContext` narrowed via `Pick<ExtensionContext, ...>` (see Design).
146
+ - Hoist `StubTheme` to module scope and reuse it as the default `createContext` theme.
147
+ - Replace remaining `theme: { fg: (_name, text) => text }` literals with `theme: new StubTheme()`.
148
+ - Cast or `satisfies`-check synthetic events against `ToolResultEvent` / `ToolCallEvent` at emit sites.
149
+ - Update the `import type { ExtensionApiLike }` line to whatever the new export name is (if exported).
150
+
151
+ ### `test/acceptance.test.ts`
152
+
153
+ - Audit for any duck-typed shape; today it does not reference `*Like` aliases (`grep` returned no hits), so the change is likely no-op.
154
+ Re-check after `src/extension.ts` lands.
155
+
156
+ ### Other tests
157
+
158
+ - `rg -l "Like\b" test/` to be sure nothing else duck-types Pi shapes.
159
+ Update if found.
160
+
161
+ ### Docs
162
+
163
+ - No changes to `docs/configuration.md`, `README.md`, `schemas/pi-autoformat.schema.json` (verified — none reference the duck-typed shapes).
164
+
165
+ ## TDD Order
166
+
167
+ This is a typing-only refactor, so the "red" step is **a failing `tsc`/`vitest` typecheck**, not a failing runtime assertion.
168
+ Every cycle ends with `pnpm test` and `pnpm exec tsc --noEmit` (or whatever typecheck script lands as part of step 1).
169
+
170
+ 1. **chore: add `@mariozechner/pi-coding-agent` devDependency.** Install via `pnpm add -D @mariozechner/pi-coding-agent@^0.72.0`. Verify `pnpm test` still passes (no source changes yet). Commit: `chore: add pi-coding-agent for runtime types`.
171
+
172
+ 2. **test: prove a plain-function `theme.fg` stub will fail to typecheck once we adopt real types.** Add a single isolated typecheck-only test (e.g. `test/types/theme-stub.test-d.ts` using `expectTypeOf` or a `// @ts-expect-error` block in a new TS file under `test/`) that asserts `{ fg: (_n, t) => t }` is **not** assignable to `Theme`.
173
+ This is currently `// @ts-expect-error` against the duck type (so it red-flags) — it goes green in step 4. Commit: `test: pin Theme stub-shape expectations`.
174
+
175
+ 3. **feat: replace `*Like` aliases in `src/extension.ts` with real Pi types.** Import real types, introduce `AutoformatExtensionContext` and `ExtensionAPIWithEvents`, swap every signature.
176
+ Tests will fail to compile here; that's expected.
177
+ Do **not** touch test files yet — this commit captures the boundary-changing diff in isolation.
178
+ *(Tip: temporarily skip `pnpm test` in this commit if needed; mark in commit body. Or fold steps 3+4 into one commit if a half-broken intermediate offends — author's call.)* Commit: `refactor: import Pi types from pi-coding-agent`.
179
+
180
+ 4. **test: update test stubs to satisfy real Pi types.** Hoist `StubTheme`, narrow `TestContext`, cast synthetic events, etc.
181
+ `pnpm test` and typecheck both green. Step 2's expectation flips from `@ts-expect-error` to a positive assertion. Commit: `test: adopt class-based Theme stubs and Pi event types`.
182
+
183
+ 5. **chore: verify lint / docs alignment.** Run `pnpm run lint` and `pnpm run lint:md`. No expected changes; confirm `schemas/pi-autoformat.schema.json`, `docs/configuration.md`, `README.md` untouched. If any drift, address in a single follow-up commit. Commit (only if needed): `docs: align after Pi types adoption`.
184
+
185
+ If steps 3 and 4 must be merged to keep CI green at every commit, do so and label the merged commit `refactor: adopt pi-coding-agent types` — but prefer the split when feasible because the test-stub diff is mechanical and noisy.
186
+
187
+ ## Risks and Mitigations
188
+
189
+ - **Risk: future Pi release breaks our build.** Mitigation: `^0.72.0` range + Renovate/Dependabot bump.
190
+ Build-time breakage is the desired tradeoff vs the runtime bug we just shipped.
191
+ - **Risk: real `ExtensionContext` requires fields we never use, churning every test stub.** Mitigation: narrow internal helpers to `Pick<ExtensionContext, "cwd" | "hasUI" | "ui">` (decision recorded above).
192
+ - **Risk: discriminated-union `ToolResultEvent` rejects our synthetic test events.** Mitigation: cast at emit sites; the test-side narrowing has no production value beyond "compiles."
193
+ - **Risk: `pi.events` is not in Pi's `ExtensionAPI`, so the real type loses the channel we depend on.** Mitigation: local `ExtensionAPIWithEvents` intersection alias, scoped to `subscribeToEventBus`, with a TODO comment to delete once Pi exposes it natively.
194
+ - **Risk: `export type { ExtensionApiLike }` is re-exported and consumed downstream.** Mitigation: `rg "ExtensionApiLike"` before deletion; if external consumers exist (none expected — this is an extension package), preserve as a deprecated alias for one release.
195
+ - **Risk: pnpm install pulls a large transitive tree.** Mitigation: it's a `devDependency`, not shipped; verify with `pnpm why`.
196
+
197
+ ## Open Questions
198
+
199
+ - Should we adopt Pi's `isBashToolResult` / `isEditToolResult` / `isWriteToolResult` type guards in `src/extension.ts` instead of `event.toolName === "..."` string checks? Out of scope for this plan; revisit if the dispatch grows or a guard would simplify a future change.
200
+ - Should the local `ExtensionAPIWithEvents` alias migrate into a shared `src/pi-types.ts` module if other modules ever need it? Defer — only one call site uses it today.
201
+ - Should we narrow `TextContent` further (e.g. require `type === "text"`)? Defer — the existing runtime check on `typeof item.text === "string"` is already the contract; tightening the type adds no value.
@@ -0,0 +1,355 @@
1
+ ---
2
+ issue: 27
3
+ issue_title: "Format before agent exit and give the agent a follow-up turn"
4
+ ---
5
+
6
+ # Format before agent exit and give the agent a follow-up turn
7
+
8
+ ## Problem Statement
9
+
10
+ Autoformatting currently runs at `agent_end`, after the agent has fully exited its turn loop.
11
+ This causes two problems:
12
+
13
+ 1. The agent discovers dirty state (formatting diffs) it did not produce on its next invocation, leading to confusion or unnecessary corrective actions.
14
+ 2. In commit-and-push workflows like `/ship-issue`, the agent commits its work before formatting runs, so the pushed commit contains unformatted code.
15
+
16
+ Both stem from the agent never getting a chance to observe or react to formatting changes.
17
+
18
+ Additionally, formatter *failures* (syntax errors, missing config) are currently reported only to the human via `ui.notify`.
19
+ The agent cannot see or fix these issues, even though it is often best positioned to do so.
20
+
21
+ Finally, the `formatMode` config field offers three timing modes (`"tool"`, `"prompt"`, `"session"`), but only `"prompt"` is meaningfully useful:
22
+
23
+ - `"tool"` formats after every tool call — nearly identical to per-turn (93% of turns have one tool call) and actively harmful in multi-tool turns where formatting between edits can corrupt subsequent `oldText` matches.
24
+ - `"session"` formats at session shutdown, when the agent is completely gone and cannot react.
25
+
26
+ Since nobody outside the project uses this extension yet, we can make a clean break.
27
+
28
+ ## Goals
29
+
30
+ 1. Remove the `formatMode` config field entirely. The runtime always uses prompt-end timing (the previous `"prompt"` behavior). The loader tolerates the legacy key, emits a config issue, and discards the value.
31
+ 2. After formatting runs at `agent_end`, give the agent one follow-up turn via `pi.sendMessage({ triggerTurn: true })` so it can see which files changed and react.
32
+ 3. Include formatter failure details (stderr, exit code) in the follow-up message so the agent can attempt to fix issues.
33
+ 4. Prevent infinite loops: at most one follow-up per user prompt.
34
+ 5. Expose the follow-up behavior as a new boolean config field (`notifyAgent`), defaulting to `false`.
35
+ 6. Skip the follow-up turn when the flush produced no groups (nothing to report).
36
+
37
+ ## Non-Goals
38
+
39
+ 1. Byte-level diffing to detect whether the formatter actually changed file content.
40
+ The initial implementation triggers the follow-up whenever the flush produces non-empty groups.
41
+ 2. Customizing the follow-up message template via config.
42
+ 3. Making `notifyAgent: true` the default (defer until real-world feedback).
43
+
44
+ ## Background
45
+
46
+ ### Relevant modules
47
+
48
+ | Module | Role |
49
+ | --- | --- |
50
+ | `src/extension.ts` | Extension entrypoint. Registers lifecycle handlers (`session_start`, `tool_result`, `agent_end`, `session_shutdown`). Owns `queueFlush()` and result reporting. Has `formatMode` branching in `tool_result` and `agent_end` handlers. |
51
+ | `src/formatter-config.ts` | Defines `FormatMode` type, `AutoformatConfig`, `UserFormatterConfig`, defaults, `createFormatterConfig()`. |
52
+ | `src/config-loader.ts` | Loads/merges global+project config, validates `formatMode` values, produces `LoadConfigResult`. |
53
+ | `src/prompt-autoformatter.ts` | `PromptAutoformatter` class. Tracks touched files, runs formatter chains, returns `PromptAutoformatterResult`. |
54
+ | `src/formatter-executor.ts` | `BatchRun` type — includes `stdout`, `stderr`, `exitCode` for each formatter invocation. |
55
+ | `schemas/pi-autoformat.schema.json` | JSON Schema for config validation. Includes `formatMode` enum. |
56
+ | `docs/configuration.md` | User-facing config documentation. Documents `formatMode` and its three values. |
57
+ | `test/extension.test.ts` | Extension lifecycle tests with `TestPi` harness. Tests for `"tool"`, `"prompt"`, `"session"` modes. |
58
+ | `test/config-loader.test.ts` | Config validation and merge tests. |
59
+
60
+ ### Pi extension API surface
61
+
62
+ ```typescript
63
+ // Current flush trigger
64
+ pi.on("agent_end", handler)
65
+
66
+ // New — triggers a follow-up agent turn
67
+ pi.sendMessage<T>(
68
+ message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
69
+ options?: {
70
+ triggerTurn?: boolean;
71
+ deliverAs?: "steer" | "followUp" | "nextTurn";
72
+ }
73
+ ): void;
74
+ ```
75
+
76
+ `pi.sendMessage` is on the `ExtensionAPI` object (the `pi` closure variable), not on the per-event `ExtensionContext`.
77
+ No new wiring is needed.
78
+
79
+ ### Session data insights
80
+
81
+ Analysis of ~1,900 assistant turns:
82
+
83
+ - 93.4% contain exactly one tool call (sequential across turns, not batched within).
84
+ - Average mutation-turn streak: 4.6 turns.
85
+ - `formatMode: "tool"` would fire nearly as often as per-turn, with negligible batching benefit and real risk of corrupting multi-tool edits.
86
+
87
+ ## Design Overview
88
+
89
+ ### Removing `formatMode`
90
+
91
+ Per AGENTS.md deprecation policy: accept the legacy key, emit a single non-fatal config issue describing the removal, and discard the value.
92
+
93
+ Concrete changes:
94
+
95
+ 1. Drop `FormatMode` type from `src/formatter-config.ts`.
96
+ 2. Drop `formatMode` from `AutoformatConfig`, `UserFormatterConfig`, `DEFAULT_FORMATTER_CONFIG`, and `createFormatterConfig()`.
97
+ 3. Drop `formatMode` from `schemas/pi-autoformat.schema.json`.
98
+ 4. Drop `formatMode` from `docs/configuration.md`.
99
+ 5. In `src/config-loader.ts`: when `formatMode` key is present in user config, emit a config issue (`formatMode has been removed; prompt-end formatting is now the only mode.`) and discard.
100
+ 6. In `src/extension.ts`: remove all `formatMode` branching. The `tool_result` handler no longer conditionally flushes. The `agent_end` handler always flushes. The `session_shutdown` handler no longer conditionally flushes (it still cleans up state).
101
+ 7. Remove tests for `"tool"` and `"session"` mode behaviors; add a test for the legacy-key config issue.
102
+
103
+ ### Sequence with `notifyAgent: true`
104
+
105
+ ```text
106
+ Turn N: agent makes final edits
107
+ Turn N+1: agent says "Done!" → agent_end fires
108
+
109
+ flush formatter (batched, always prompt-end)
110
+
111
+ flush produced groups?
112
+ ├─ no → done, no follow-up
113
+ └─ yes → compose message (successes + failures)
114
+ pi.sendMessage({ customType: "autoformat-notify", ... },
115
+ { triggerTurn: true })
116
+
117
+ Turn N+2: agent sees notification, can react (commit, fix failures, acknowledge)
118
+
119
+ agent_end fires again
120
+
121
+ flush (any new touched files from Turn N+2?)
122
+ ├─ groups → format them, but do NOT send another follow-up (loop guard)
123
+ └─ empty → done
124
+ ```
125
+
126
+ ### Loop guard
127
+
128
+ A `followUpPending` flag on `SessionState` prevents unbounded re-triggering:
129
+
130
+ 1. On `agent_end` entry: read `followUpPending` into a local, then set it to `false`.
131
+ 2. After flush: if result has groups AND the locally-read value was `false`, send the message and set `followUpPending = true`.
132
+ 3. Effect: first `agent_end` → format + notify. Second `agent_end` → format if needed, no notify.
133
+
134
+ The flag resets naturally because step 1 always clears it on entry.
135
+
136
+ ### Follow-up message content
137
+
138
+ The message body combines successes and failures into one agent-readable block:
139
+
140
+ ```text
141
+ [autoformat] Formatted 3 file(s): src/foo.ts, src/bar.ts, README.md
142
+
143
+ Failures:
144
+ prettier (exit 2) on src/broken.ts:
145
+ stderr: SyntaxError: Unexpected token at line 42
146
+ ```
147
+
148
+ Specifics:
149
+
150
+ - File list truncated at 10, with "… and N more" suffix.
151
+ - Failure details include `stderr` (trimmed by the existing `formatterOutput` config limits).
152
+ - `stdout` included only when `formatterOutput.onFailure` is `"both"`.
153
+ - When all runs succeeded, the failures section is omitted.
154
+ - When all runs failed (no successes), the success line is omitted.
155
+ - `customType` is `"autoformat-notify"` for identification in session logs and potential custom rendering.
156
+
157
+ ### Config shape
158
+
159
+ ```typescript
160
+ // UserFormatterConfig (optional)
161
+ notifyAgent?: boolean;
162
+
163
+ // AutoformatConfig (resolved)
164
+ notifyAgent: boolean; // default: false
165
+ ```
166
+
167
+ ### `TestPi` harness changes
168
+
169
+ Add `sendMessage` capture to `TestPi`:
170
+
171
+ ```typescript
172
+ readonly sentMessages: Array<{
173
+ message: { customType?: string; content?: string };
174
+ options?: { triggerTurn?: boolean };
175
+ }> = [];
176
+
177
+ readonly sendMessage = ((message: unknown, options?: unknown) => {
178
+ this.sentMessages.push({ message, options });
179
+ }) as ExtensionAPI["sendMessage"];
180
+ ```
181
+
182
+ ## Module-Level Changes
183
+
184
+ ### `src/formatter-config.ts`
185
+
186
+ 1. Remove `FormatMode` type.
187
+ 2. Remove `formatMode` from `UserFormatterConfig`, `AutoformatConfig`, `DEFAULT_FORMATTER_CONFIG`.
188
+ 3. Remove `formatMode` from `createFormatterConfig()`.
189
+ 4. Add `notifyAgent?: boolean` to `UserFormatterConfig`.
190
+ 5. Add `notifyAgent: boolean` to `AutoformatConfig` (default: `false`).
191
+ 6. Wire `notifyAgent` through `createFormatterConfig()`.
192
+
193
+ ### `src/config-loader.ts`
194
+
195
+ 1. Remove `validateFormatMode()`.
196
+ 2. When `formatMode` key is present, emit a config issue and discard.
197
+ 3. Add `notifyAgent` boolean validation.
198
+ 4. Wire `notifyAgent` through `mergeUserConfigs()`.
199
+
200
+ ### `schemas/pi-autoformat.schema.json`
201
+
202
+ 1. Remove `formatMode` property.
203
+ 2. Add `notifyAgent` boolean property.
204
+
205
+ ### `src/extension.ts`
206
+
207
+ 1. Remove `formatMode` branching from `tool_result` handler (no more conditional flush).
208
+ 2. Remove `formatMode !== "prompt"` guard from `agent_end` handler (always flush).
209
+ 3. Remove `formatMode === "session"` conditional from `session_shutdown` handler.
210
+ 4. Add `followUpPending: boolean` to `SessionState`.
211
+ 5. Extract `buildNotifyMessageContent(result, config): string | undefined` helper.
212
+ 6. In `agent_end` handler: after flush, conditionally call `pi.sendMessage`.
213
+
214
+ ### `docs/configuration.md`
215
+
216
+ 1. Remove `formatMode` section.
217
+ 2. Add `notifyAgent` section.
218
+
219
+ ### `README.md`
220
+
221
+ 1. Remove any `formatMode` references.
222
+ 2. Add `notifyAgent` mention.
223
+
224
+ ### `test/extension.test.ts`
225
+
226
+ 1. Remove tests for `"tool"` mode and `"session"` mode behavior.
227
+ 2. Remove `formatMode` parameter from `createLoadResult()` helper.
228
+ 3. Extend `TestPi` with `sendMessage` capture.
229
+ 4. Add follow-up turn tests.
230
+
231
+ ### `test/config-loader.test.ts`
232
+
233
+ 1. Remove `formatMode` validation tests (valid values, invalid values).
234
+ 2. Add test for legacy `formatMode` key producing a config issue.
235
+ 3. Add `notifyAgent` validation tests.
236
+
237
+ ## TDD Order
238
+
239
+ ### 1. Remove `formatMode` from config types and defaults
240
+
241
+ - **Test surface:** `test/formatter-config.test.ts` or `test/config-loader.test.ts`.
242
+ - **Covers:** `FormatMode` type removed; `createFormatterConfig()` no longer accepts or produces `formatMode`; existing tests that reference `formatMode` are updated or removed.
243
+ - **Commit:** `feat!: remove formatMode config field (#27)`
244
+
245
+ ### 2. Legacy `formatMode` key tolerance in config loader
246
+
247
+ - **Test surface:** `test/config-loader.test.ts`.
248
+ - **Covers:** Config containing `formatMode: "prompt"` (or any value) is accepted without error but emits a config issue. The value is discarded.
249
+ - **Commit:** `feat: emit config issue for legacy formatMode key (#27)`
250
+
251
+ ### 3. Remove `formatMode` from JSON schema
252
+
253
+ - **Test surface:** `test/schema.test.ts`.
254
+ - **Covers:** Schema no longer includes `formatMode`. Configs with `formatMode` pass validation (via `additionalProperties` tolerance or explicit handling).
255
+ - **Commit:** `feat!: remove formatMode from config schema (#27)`
256
+
257
+ ### 4. Remove `formatMode` branching from extension runtime
258
+
259
+ - **Test surface:** `test/extension.test.ts`.
260
+ - **Covers:** Remove `"tool"` and `"session"` mode tests. `tool_result` handler no longer flushes. `agent_end` always flushes. `session_shutdown` no longer conditionally flushes. Update `createLoadResult()` helper to drop the `formatMode` parameter.
261
+ - **Commit:** `feat!: always use prompt-end formatting (#27)`
262
+
263
+ ### 5. `notifyAgent` config field — types, defaults, schema, loader
264
+
265
+ - **Test surface:** `test/config-loader.test.ts`, `test/schema.test.ts`.
266
+ - **Covers:** Defaults to `false`; user-supplied `true` preserved; schema accepts boolean; non-boolean rejected.
267
+ - **Commit:** `feat: add notifyAgent config field (#27)`
268
+
269
+ ### 6. Notification message builder
270
+
271
+ - **Test surface:** `test/extension.test.ts` (or extracted `test/notify-message.test.ts`).
272
+ - **Covers:** Message text with 1 file, 3 files, 11 files (truncation at 10), 0 groups (returns `undefined`). Message with mixed success/failure including stderr. Message with all-failures (no success line).
273
+ - **Commit:** `feat: add buildNotifyMessageContent helper (#27)`
274
+
275
+ ### 7. `TestPi` harness — add `sendMessage` capture
276
+
277
+ - **Test surface:** `test/extension.test.ts`.
278
+ - **Covers:** Refactor only — `TestPi` records `sendMessage` calls. No behavioral change to existing tests.
279
+ - **Commit:** `test: extend TestPi with sendMessage capture (#27)`
280
+
281
+ ### 8. Follow-up turn on successful flush
282
+
283
+ - **Test surface:** `test/extension.test.ts`.
284
+ - **Covers:** `notifyAgent: true` + flush with successful groups → `pi.sendMessage` called once with `{ triggerTurn: true }`, `customType: "autoformat-notify"`, and message containing file names.
285
+ - **Commit:** `feat: send follow-up turn after formatting (#27)`
286
+
287
+ ### 9. Follow-up includes failure details
288
+
289
+ - **Test surface:** `test/extension.test.ts`.
290
+ - **Covers:** Flush with mixed success/failure → follow-up message includes stderr and exit code for failed runs.
291
+ - **Commit:** `feat: include formatter failures in follow-up message (#27)`
292
+
293
+ ### 10. No follow-up on empty flush
294
+
295
+ - **Test surface:** `test/extension.test.ts`.
296
+ - **Covers:** Empty flush → `sendMessage` not called.
297
+ - **Commit:** `test: no follow-up on empty flush (#27)`
298
+
299
+ ### 11. No follow-up when `notifyAgent` is `false`
300
+
301
+ - **Test surface:** `test/extension.test.ts`.
302
+ - **Covers:** Default config → `sendMessage` not called even with successful groups.
303
+ - **Commit:** `test: notifyAgent false suppresses follow-up (#27)`
304
+
305
+ ### 12. Loop guard — at most one follow-up per user prompt
306
+
307
+ - **Test surface:** `test/extension.test.ts`.
308
+ - **Covers:** Simulate two consecutive `agent_end` events. `sendMessage` called exactly once.
309
+ - **Commit:** `test: follow-up turn loop guard (#27)`
310
+
311
+ ### 13. Follow-up resets across user prompts
312
+
313
+ - **Test surface:** `test/extension.test.ts`.
314
+ - **Covers:** After a full cycle (agent_end → follow-up → agent_end), a new agent_end with fresh tool results triggers a new follow-up.
315
+ - **Commit:** `test: follow-up resets across prompts (#27)`
316
+
317
+ ### 14. Documentation
318
+
319
+ - **Test surface:** Manual review.
320
+ - **Covers:** `docs/configuration.md` (remove `formatMode`, add `notifyAgent`), `README.md`, schema description.
321
+ - **Commit:** `docs: document notifyAgent and remove formatMode docs (#27)`
322
+
323
+ ## Risks and Mitigations
324
+
325
+ ### Infinite follow-up loop
326
+
327
+ The `followUpPending` flag ensures at most one follow-up per user prompt.
328
+ Even if the follow-up turn triggers new edits, the second `agent_end` formats them but does not re-trigger.
329
+
330
+ ### `sendMessage` in non-interactive mode
331
+
332
+ `pi.sendMessage` is on `ExtensionAPI`, which is always present.
333
+ In RPC/print mode, `triggerTurn` may be a no-op.
334
+ The extension sends the message regardless and lets Pi decide delivery.
335
+ If problematic, a follow-up can add a `ctx.hasUI` guard.
336
+
337
+ ### Extra token cost
338
+
339
+ The follow-up turn requires one LLM call.
340
+ Usually a short response.
341
+ The feature is opt-in (`notifyAgent: false`), so cost-sensitive users are unaffected.
342
+
343
+ ### Agent makes new edits during follow-up
344
+
345
+ Expected and handled: the second `agent_end` runs the formatter on new touched files.
346
+ The loop guard prevents a third follow-up.
347
+
348
+ ## Open Questions
349
+
350
+ 1. Should `notifyAgent` eventually default to `true`?
351
+ Defer until real-world feedback confirms reliability and acceptable token cost.
352
+ 2. Should the follow-up turn be skipped when formatting produced no byte-level changes?
353
+ Defer — requires reading files before/after, adding I/O overhead.
354
+ 3. Should the follow-up message include a diff summary (lines changed)?
355
+ Defer — file names and failure details are sufficient for the agent to decide whether to act.