@gotgenes/pi-autoformat 0.1.0

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.
@@ -0,0 +1,402 @@
1
+ # Pi Autoformat: Initial Implementation Plan
2
+
3
+ ## Problem Statement
4
+
5
+ Pi agents frequently modify files that later fail commit-time hooks because formatting was not run after editing. This is especially painful for formatters that mutate files, such as Prettier and Markdown lint fixers.
6
+
7
+ In practice, this creates a bad workflow:
8
+
9
+ 1. The agent edits files.
10
+ 2. The agent believes the task is complete.
11
+ 3. A commit or pre-commit hook runs later and mutates files.
12
+ 4. The commit fails because files changed during commit validation.
13
+ 5. The agent must recover from late, out-of-band file mutations.
14
+
15
+ This is made worse in repositories that use `prek` for pre-commit hooks, because `prek` does not automatically re-stage files after fixer commands mutate them.
16
+
17
+ The goal of this repository is to provide a Pi-native auto-formatting solution that runs *before* commit time, so agents do not need to remember formatter commands and do not get surprised by late formatting changes.
18
+
19
+ ## Desired Outcome
20
+
21
+ A Pi extension that:
22
+
23
+ - automatically formats files changed by the agent
24
+ - reduces or eliminates commit failures caused only by missing formatting
25
+ - works with project-specific formatter commands
26
+ - defaults to a timing model that is safe for Pi's edit workflow
27
+ - makes formatter failures visible without blocking normal editing
28
+
29
+ ## Prior Research Summary
30
+
31
+ ### Pi extension capabilities relevant to this problem
32
+
33
+ Pi already exposes the mechanisms needed to implement this as an extension.
34
+
35
+ Useful extension hooks and features:
36
+
37
+ - tool lifecycle hooks such as `tool_result`
38
+ - turn/agent lifecycle hooks such as `turn_end` / `agent_end`
39
+ - built-in tool override support
40
+ - project-local extensions in `.pi/extensions/`
41
+ - per-file mutation coordination via `withFileMutationQueue()`
42
+
43
+ Important implementation observation:
44
+
45
+ - formatting immediately after every `write`/`edit` is possible
46
+ - but it can create follow-up edit failures if formatting changes the file before later exact-text edits operate on it
47
+
48
+ That makes deferred formatting after the agent finishes a prompt materially safer than immediate per-tool formatting.
49
+
50
+ ### Findings from `tenzir/pi-formatter`
51
+
52
+ `pi-formatter` is close to the needed solution and validates the general approach.
53
+
54
+ What it does well:
55
+
56
+ - hooks successful `write` and `edit` tool results
57
+ - supports multiple timing modes: `tool`, `prompt`, `session`
58
+ - defaults to `prompt`, which is safer than `tool`
59
+ - keeps formatter failures non-blocking
60
+ - provides TUI summaries for formatter results
61
+
62
+ Important gaps:
63
+
64
+ - only covers Pi `write` and `edit`, not arbitrary file mutations from `bash` or custom mutating tools
65
+ - built-in formatter set is opinionated
66
+ - does not directly model "run the exact formatter chain this repository already wants"
67
+ - does not specifically support `markdownlint-cli2` out of the box
68
+
69
+ Key design takeaway:
70
+
71
+ - deferred formatting after the agent finishes a prompt is the correct default timing model for Pi
72
+
73
+ ### Findings from OpenCode's built-in formatter system
74
+
75
+ OpenCode solves several problems better than `pi-formatter`.
76
+
77
+ What OpenCode does well:
78
+
79
+ - formatting is built into core mutation tools, not just an add-on hook
80
+ - it formats from `write`, `edit`, and `apply_patch`
81
+ - it supports a config-driven formatter registry
82
+ - custom formatters can define:
83
+ - command
84
+ - environment
85
+ - file extensions
86
+ - multiple matching formatters can run sequentially for the same file
87
+ - built-in coverage is broad
88
+ - formatter definitions are project-oriented rather than hardcoded to a small fixed tool set
89
+
90
+ Important remaining gaps in OpenCode:
91
+
92
+ - formatting still happens immediately after individual tool calls
93
+ - that eager timing can still create stale-file drift for later edits
94
+ - it still does not automatically cover arbitrary shell-driven file mutations
95
+ - formatter failures are logged, but reporting is less explicit than `pi-formatter`
96
+
97
+ Key design takeaway:
98
+
99
+ - OpenCode's formatter registry/config model is worth borrowing
100
+ - OpenCode's immediate execution timing is *not* the best default for Pi
101
+
102
+ ## Design Direction
103
+
104
+ The recommended architecture is a hybrid of the best ideas from both systems:
105
+
106
+ - use a Pi extension
107
+ - use a config-driven formatter registry inspired by OpenCode
108
+ - default formatting timing to end-of-prompt, inspired by `pi-formatter`
109
+ - optionally support end-of-session and immediate-per-tool modes
110
+ - add clearer support for repository-specific formatter commands and formatter chains
111
+
112
+ ## Proposed Scope
113
+
114
+ ### In scope
115
+
116
+ - project-local or globally installable Pi extension
117
+ - automatic formatting for files touched by Pi's built-in mutation tools
118
+ - configurable formatter registry
119
+ - support for custom formatter commands
120
+ - support for multiple formatters per file type in declared order
121
+ - visible summaries or warnings for formatter success/failure
122
+ - default safe timing mode for Pi agents
123
+
124
+ ### Out of scope for the first version
125
+
126
+ - perfect detection of every file mutated by arbitrary shell commands
127
+ - automatic staging or commit orchestration
128
+ - replacing existing pre-commit hooks
129
+ - whole-repository formatting after every response
130
+
131
+ ## Core Product Decisions
132
+
133
+ ### 1. Default timing mode
134
+
135
+ Default to formatting once after the agent finishes a prompt.
136
+
137
+ Rationale:
138
+
139
+ - safer for Pi's edit workflow than formatting after every tool call
140
+ - avoids mutating a file between sibling edits in the same assistant run
141
+ - still happens early enough to prevent most commit-time failures
142
+
143
+ Optional modes can be added later:
144
+
145
+ - `tool`: format immediately after each successful mutation tool
146
+ - `prompt`: format once after agent work completes for the prompt
147
+ - `session`: accumulate touched files and format on session shutdown
148
+
149
+ ### 2. Formatter model
150
+
151
+ Use a configurable formatter registry.
152
+
153
+ Each formatter entry should be able to specify at least:
154
+
155
+ - `command: string[]`
156
+ - `environment?: Record<string, string>`
157
+ - `extensions: string[]`
158
+ - `disabled?: boolean`
159
+
160
+ Likely additions beyond OpenCode:
161
+
162
+ - explicit `order` or ordered array semantics
163
+ - optional `when` or config-detection behavior for built-ins
164
+ - optional `mode` for chain behavior, e.g. `all` vs `first-success` vs `fallback`
165
+
166
+ ### 3. Formatter chain behavior
167
+
168
+ Support multiple formatters for the same file type in explicit order.
169
+
170
+ This matters for repositories that want things like:
171
+
172
+ - `prettier --write`
173
+ - `markdownlint-cli2 --fix`
174
+
175
+ or other repo-specific chains.
176
+
177
+ Do not rely on object insertion order alone if avoidable.
178
+
179
+ ### 4. Failure behavior
180
+
181
+ Formatter failures should not block the original edit/write result by default.
182
+
183
+ However, failures should be surfaced clearly:
184
+
185
+ - TUI summary lines when interactive
186
+ - warning text or logs when non-interactive
187
+ - clear indication of which file and formatter failed
188
+
189
+ ### 5. File coverage strategy
190
+
191
+ Initial implementation should cover at least:
192
+
193
+ - `write`
194
+ - `edit`
195
+
196
+ Potential next step:
197
+
198
+ - support additional mutation tools, if present
199
+ - add optional touched-file collection for custom tools
200
+ - evaluate whether shell-driven file mutation support is practical without introducing too much complexity or noise
201
+
202
+ ## Suggested Configuration Shape
203
+
204
+ Use extension-owned config files instead of Pi `settings.json` keys.
205
+
206
+ Recommended locations:
207
+
208
+ - global: `~/.pi/agent/extensions/pi-autoformat/config.json`
209
+ - project: `.pi/extensions/pi-autoformat/config.json`
210
+
211
+ Project config should override global config.
212
+
213
+ Example draft only:
214
+
215
+ ```json
216
+ {
217
+ "$schema": "https://raw.githubusercontent.com/gotgenes/pi-autoformat/main/schemas/pi-autoformat.schema.json",
218
+ "formatMode": "prompt",
219
+ "hideSummariesInTui": false,
220
+ "formatters": {
221
+ "prettier": {
222
+ "command": ["prettier", "--write", "$FILE"],
223
+ "extensions": [".js", ".ts", ".tsx", ".json", ".md"]
224
+ },
225
+ "markdownlint-cli2": {
226
+ "command": ["markdownlint-cli2", "--fix", "$FILE"],
227
+ "extensions": [".md"]
228
+ }
229
+ },
230
+ "chains": {
231
+ ".md": ["prettier", "markdownlint-cli2"],
232
+ ".ts": ["prettier"]
233
+ }
234
+ }
235
+ ```
236
+
237
+ Notes:
238
+
239
+ - `$FILE` substitution is simple and proven
240
+ - a separate `chains` section may be clearer than relying only on formatter extension overlap
241
+ - built-in formatters can exist, but project config should be able to override them cleanly
242
+ - owning a dedicated config file makes it straightforward to publish a JSON Schema for editor validation and autocompletion
243
+
244
+ ## Implementation Plan
245
+
246
+ ### Phase 1: repository and extension skeleton
247
+
248
+ Status: complete.
249
+
250
+ Completed:
251
+
252
+ - created package skeleton for a Pi extension package
253
+ - defined config file locations:
254
+ - global: `~/.pi/agent/extensions/pi-autoformat/config.json`
255
+ - project: `.pi/extensions/pi-autoformat/config.json`
256
+ - defined config merge behavior with project overriding global
257
+ - published a JSON Schema for the config file at `schemas/pi-autoformat.schema.json`
258
+ - documented configuration in `docs/configuration.md`
259
+ - added a README describing the problem, approach, install, and config
260
+ - added `LICENSE`
261
+ - added the Pi extension entry point and package wiring
262
+
263
+ ### Phase 2: touched-file collection and flush timing
264
+
265
+ Status: complete for v1 scope.
266
+
267
+ Completed:
268
+
269
+ - watchable touched-file collection primitives for `write` and `edit`
270
+ - path resolution, normalization, and prompt-local deduping
271
+ - prompt-end flush behavior in the core autoformatter
272
+ - per-file sequential execution flow
273
+
274
+ Deferred beyond v1:
275
+
276
+ - consider whether additional mutation sources should feed the touched-file queue
277
+
278
+ ### Phase 3: formatter registry
279
+
280
+ Status: complete for v1 scope.
281
+
282
+ Completed:
283
+
284
+ - built-in formatter definitions
285
+ - custom formatter config parsing and validation
286
+ - formatter resolution by file
287
+ - `$FILE` substitution
288
+ - command execution with optional environment overrides
289
+ - v1 formatter command resolution documented as cwd/environment-driven with explicit command overrides for wrapper-based workflows
290
+
291
+ ### Phase 4: formatter chain execution
292
+
293
+ Status: complete for v1 scope.
294
+
295
+ Completed:
296
+
297
+ - ordered execution for formatter chains
298
+ - sequential chain behavior
299
+ - per-run success/failure capture
300
+ - explicit `chains`-only execution behavior for v1
301
+
302
+ ### Phase 5: reporting
303
+
304
+ Status: complete for v1 scope.
305
+
306
+ Completed:
307
+
308
+ - interactive notifications for formatter summaries
309
+ - non-interactive warning/info logging
310
+ - concise file-level failure reporting
311
+ - config-driven hiding of success summaries in interactive mode
312
+ - focused reporting coverage for interactive and non-interactive modes
313
+
314
+ Deferred beyond v1:
315
+
316
+ - richer TUI presentation beyond basic notifications
317
+ - optional exposure of full formatter stdout/stderr in summaries
318
+
319
+ ### Phase 6: tests
320
+
321
+ Status: complete for v1 scope.
322
+
323
+ Completed:
324
+
325
+ - no formatter configured => no-op
326
+ - prompt-mode batching behavior
327
+ - sequential formatter chains for one file
328
+ - custom formatter command overrides
329
+ - formatter failure reporting without blocking edits
330
+ - deduping touched files within the same prompt
331
+ - path normalization and scope handling
332
+ - config loading, merge precedence, and validation issue reporting
333
+ - extension lifecycle tests for tool-result collection and flush timing
334
+ - reporting tests for interactive and non-interactive modes
335
+
336
+ ### Phase 7: optional enhancements
337
+
338
+ Still optional and not yet started:
339
+
340
+ - session mode
341
+ - tool mode
342
+ - support for more mutation tools
343
+ - optional shell mutation integration strategy
344
+ - optional settings command / config editor UI
345
+
346
+ ## Remaining Work Summary
347
+
348
+ The planned v1 work is complete.
349
+
350
+ Post-v1 follow-up work is tracked in GitHub issues for:
351
+
352
+ 1. richer TUI formatter summaries
353
+ 2. optional detailed formatter output in reports
354
+ 3. support for additional Pi mutation tools
355
+ 4. shell-driven mutation coverage investigation
356
+ 5. settings/config editor UI
357
+ 6. optional strict mode for formatter failures
358
+
359
+ ## Risks and Mitigations
360
+
361
+ ### Risk: formatting changes break later exact edits
362
+
363
+ Mitigation:
364
+
365
+ - default to prompt-end formatting, not per-tool formatting
366
+ - document that `tool` mode is less safe
367
+
368
+ ### Risk: formatter chains create unexpected file churn
369
+
370
+ Mitigation:
371
+
372
+ - explicit ordering
373
+ - only format touched files
374
+ - clear reporting
375
+
376
+ ### Risk: shell-driven file mutations remain uncovered
377
+
378
+ Mitigation:
379
+
380
+ - treat as a known limitation in v1
381
+ - design extension internals so more mutation sources can be added later
382
+
383
+ ### Risk: formatter failures become invisible
384
+
385
+ Mitigation:
386
+
387
+ - always capture per-file formatter results
388
+ - surface warnings in interactive and non-interactive contexts
389
+
390
+ ## Open Questions
391
+
392
+ These questions have now been answered for v1:
393
+
394
+ 1. Answered: built-in formatter resolution should stay simple in v1 and rely on cwd/environment plus explicit command overrides, rather than trying to auto-detect and invoke project-local tools.
395
+ 2. Answered: v1 should use explicit `chains` only for formatter ordering and execution, rather than extension-overlap fallback behavior.
396
+ 3. Answered: shell-driven mutation coverage is excluded from v1 and should remain a documented limitation for now.
397
+ 4. Answered: formatter failures should remain visible but non-blocking in v1; no strict mode should be added yet.
398
+ 5. Answered: schema URL guidance should document both default-branch and pinned-tag options.
399
+
400
+ ## Recommended Next Milestone
401
+
402
+ Ship the v1 release, then continue with the deferred follow-up work tracked in GitHub issues.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@gotgenes/pi-autoformat",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension package for prompt-end auto-formatting",
5
+ "author": {
6
+ "name": "Chris Lasher"
7
+ },
8
+ "license": "MIT",
9
+ "type": "module",
10
+ "pi": {
11
+ "extensions": [
12
+ "./src/extension.ts"
13
+ ]
14
+ },
15
+ "packageManager": "pnpm@10.33.2",
16
+ "scripts": {
17
+ "lint": "pnpm exec biome check . && pnpm run lint:md",
18
+ "lint:fix": "pnpm exec biome check --write --files-ignore-unknown=true . && pnpm run lint:md:fix",
19
+ "lint:md": "pnpm exec markdownlint-cli2 '*.md' 'docs/**/*.md'",
20
+ "lint:md:fix": "pnpm exec markdownlint-cli2 --fix '*.md' 'docs/**/*.md'",
21
+ "format": "pnpm exec biome check --write --files-ignore-unknown=true .",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest"
24
+ },
25
+ "devDependencies": {
26
+ "@biomejs/biome": "^2.4.13",
27
+ "@types/node": "^25.6.0",
28
+ "markdownlint-cli2": "^0.22.1",
29
+ "typescript": "^6.0.3",
30
+ "vitest": "^4.1.5"
31
+ }
32
+ }
package/prek.toml ADDED
@@ -0,0 +1,24 @@
1
+ # Configuration file for `prek`, a git hook framework written in Rust.
2
+ # See https://prek.j178.dev for more information.
3
+ #:schema https://www.schemastore.org/prek.json
4
+
5
+ [[repos]]
6
+ repo = "builtin"
7
+ hooks = [
8
+ { id = "trailing-whitespace" },
9
+ { id = "end-of-file-fixer" },
10
+ { id = "check-added-large-files" },
11
+ ]
12
+
13
+ [[repos]]
14
+ repo = "local"
15
+ hooks = [
16
+ { id = "biome", name = "biome", entry = "pnpm exec biome check --write --files-ignore-unknown=true", language = "system", types_or = ["javascript", "jsx", "ts", "tsx", "json"] },
17
+ ]
18
+
19
+ [[repos]]
20
+ repo = "https://github.com/DavidAnson/markdownlint-cli2"
21
+ rev = "v0.22.1"
22
+ hooks = [
23
+ { id = "markdownlint-cli2", args = ["--fix"] },
24
+ ]
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {}
5
+ },
6
+ "include-v-in-tag": true,
7
+ "include-component-in-tag": false,
8
+ "release-type": "node",
9
+ "changelog-sections": [
10
+ { "type": "feat", "section": "Features" },
11
+ { "type": "fix", "section": "Bug Fixes" },
12
+ { "type": "perf", "section": "Performance Improvements" },
13
+ { "type": "revert", "section": "Reverts" },
14
+ { "type": "docs", "section": "Documentation" },
15
+ { "type": "style", "section": "Styles", "hidden": true },
16
+ { "type": "chore", "section": "Miscellaneous Chores" },
17
+ { "type": "refactor", "section": "Code Refactoring", "hidden": true },
18
+ { "type": "test", "section": "Tests", "hidden": true },
19
+ { "type": "build", "section": "Build System", "hidden": true },
20
+ { "type": "ci", "section": "Continuous Integration", "hidden": true }
21
+ ]
22
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/gotgenes/pi-autoformat/main/schemas/pi-autoformat.schema.json",
4
+ "title": "pi-autoformat configuration",
5
+ "description": "Configuration for the pi-autoformat Pi extension.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "description": "Optional schema URL for editor validation and autocomplete."
12
+ },
13
+ "formatMode": {
14
+ "type": "string",
15
+ "enum": ["tool", "prompt", "session"],
16
+ "description": "When formatting should run."
17
+ },
18
+ "commandTimeoutMs": {
19
+ "type": "integer",
20
+ "minimum": 1,
21
+ "description": "Timeout in milliseconds for each formatter command."
22
+ },
23
+ "hideSummariesInTui": {
24
+ "type": "boolean",
25
+ "description": "Hide formatter summaries in the interactive TUI."
26
+ },
27
+ "formatters": {
28
+ "type": "object",
29
+ "description": "Formatter registry keyed by formatter name.",
30
+ "additionalProperties": {
31
+ "$ref": "#/$defs/formatterDefinition"
32
+ }
33
+ },
34
+ "chains": {
35
+ "type": "object",
36
+ "description": "Ordered formatter chains keyed by file extension.",
37
+ "propertyNames": {
38
+ "type": "string",
39
+ "pattern": "^\\..+"
40
+ },
41
+ "additionalProperties": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "string",
45
+ "minLength": 1
46
+ }
47
+ }
48
+ }
49
+ },
50
+ "$defs": {
51
+ "formatterDefinition": {
52
+ "type": "object",
53
+ "additionalProperties": false,
54
+ "properties": {
55
+ "command": {
56
+ "type": "array",
57
+ "description": "Command argv used to format a file. Use $FILE as the file placeholder.",
58
+ "items": {
59
+ "type": "string"
60
+ },
61
+ "minItems": 1
62
+ },
63
+ "extensions": {
64
+ "type": "array",
65
+ "description": "File extensions this formatter applies to.",
66
+ "items": {
67
+ "type": "string",
68
+ "pattern": "^\\..+"
69
+ },
70
+ "minItems": 1,
71
+ "uniqueItems": true
72
+ },
73
+ "environment": {
74
+ "type": "object",
75
+ "description": "Environment variables to set when running the formatter.",
76
+ "additionalProperties": {
77
+ "type": "string"
78
+ }
79
+ },
80
+ "disabled": {
81
+ "type": "boolean",
82
+ "description": "Disable this formatter entry."
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }