@gotgenes/pi-autoformat 0.1.0 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -3
- package/.github/workflows/release-please.yml +29 -0
- package/.markdownlint-cli2.yaml +14 -2
- package/.pi/extensions/pi-autoformat/config.json +3 -6
- package/.pi/prompts/README.md +59 -0
- package/.pi/prompts/plan-issue.md +64 -0
- package/.pi/prompts/retro.md +144 -0
- package/.pi/prompts/ship-issue.md +77 -0
- package/.pi/prompts/tdd-plan.md +67 -0
- package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +39 -0
- package/CHANGELOG.md +365 -0
- package/README.md +42 -109
- package/biome.json +1 -1
- package/docs/assets/logo.png +0 -0
- package/docs/assets/logo.svg +533 -0
- package/docs/configuration.md +358 -38
- package/docs/plans/0001-initial-implementation-plan.md +17 -9
- package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
- package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
- package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
- package/docs/plans/0010-acceptance-test-coverage.md +240 -0
- package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
- package/docs/plans/0013-fallback-chain-step-type.md +280 -0
- package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
- package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
- package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
- package/docs/plans/0022-pi-coding-agent-types.md +201 -0
- package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
- package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
- package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
- package/docs/retro/0013-fallback-chain-step-type.md +67 -0
- package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
- package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
- package/docs/retro/0022-pi-coding-agent-types.md +62 -0
- package/docs/testing.md +95 -0
- package/package.json +30 -11
- package/prek.toml +2 -2
- package/schemas/pi-autoformat.schema.json +145 -21
- package/src/builtin-formatters.ts +205 -0
- package/src/command-probe.ts +66 -0
- package/src/config-loader.ts +829 -90
- package/src/custom-mutation-tools.ts +125 -0
- package/src/extension.ts +469 -82
- package/src/format-scope.ts +118 -0
- package/src/formatter-config.ts +73 -36
- package/src/formatter-executor.ts +230 -34
- package/src/formatter-output-report.ts +149 -0
- package/src/formatter-registry.ts +139 -30
- package/src/index.ts +26 -5
- package/src/prompt-autoformatter.ts +148 -23
- package/src/shell-mutation-detector.ts +572 -0
- package/src/touched-files-queue.ts +72 -11
- package/test/acceptance-event-bus.test.ts +138 -0
- package/test/acceptance.test.ts +69 -0
- package/test/builtin-formatters.test.ts +382 -0
- package/test/command-probe.test.ts +79 -0
- package/test/config-loader.test.ts +640 -21
- package/test/custom-mutation-tools.test.ts +190 -0
- package/test/extension.test.ts +1535 -158
- package/test/fallback-acceptance.test.ts +98 -0
- package/test/fixtures/event-bus-emitter.ts +26 -0
- package/test/fixtures/formatter-recorder.mjs +25 -0
- package/test/format-scope.test.ts +139 -0
- package/test/formatter-config.test.ts +56 -5
- package/test/formatter-executor.test.ts +555 -35
- package/test/formatter-output-report.test.ts +178 -0
- package/test/formatter-registry.test.ts +330 -37
- package/test/helpers/rpc.ts +146 -0
- package/test/prompt-autoformatter.test.ts +315 -22
- package/test/schema.test.ts +149 -0
- package/test/shell-mutation-detector.test.ts +221 -0
- package/test/touched-files-queue.test.ts +40 -1
- package/test/types/theme-stub.test-d.ts +42 -0
|
@@ -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`
|