mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
114 lines
11 KiB
Markdown
114 lines
11 KiB
Markdown
---
|
||
issue: 283
|
||
issue_title: "Formatter extension seam for custom tool input previews"
|
||
---
|
||
|
||
# Retro: #283 — Formatter extension seam for custom tool input previews
|
||
|
||
## Stage: Planning (2026-05-31T00:00:00Z)
|
||
|
||
### Session summary
|
||
|
||
Produced a numbered implementation plan for the tool input formatter seam.
|
||
Confirmed both prerequisites (`#282` extract `ToolPreviewFormatter`, `#266` configurable limits) are shipped/closed, then designed a persistent `ToolInputFormatterRegistry`, a seam-first dispatch in `formatToolInputForPrompt`, a `registerToolInputFormatter` method on `PermissionsService`, and a reference built-in MCP input summarizer registered through the public seam.
|
||
|
||
### Observations
|
||
|
||
- Despite the dual `pkg:` label, the user confirmed this is **pi-permission-system only** — pi-subagents would reach outward to register, violating its "arrows point inward" principle, so the plan is filed in the package's `docs/plans/` beside `#266`/`#282` rather than the repo-root `docs/plans/`.
|
||
- `ToolPreviewFormatter` is constructed **fresh per tool call** (from `this.session.config`), so the formatter registry cannot be instance state on it — it must be owned by the extension factory (`index.ts`) and threaded in.
|
||
This shaped the whole design.
|
||
- The seam convention follows pi-subagents' `registerWorkspaceProvider(provider): () => void` (single provider, throws on duplicate, identity-guarded disposer).
|
||
Adopted the same: one formatter per tool name, duplicate `register` throws.
|
||
- Reference built-in decision: user chose the **MCP summarizer keyed to `mcp`** over a fictional `batch` tool.
|
||
Important catch — MCP calls take an early-return branch in `formatAskPrompt` and never reach `formatToolInputForPrompt`, so the built-in needs a **second integration point** in the MCP branch (and changes existing MCP prompt tests).
|
||
Captured as a dedicated TDD step.
|
||
- Precedence: registered formatter checked first for any tool; `undefined` falls through to the existing switch (user-selected).
|
||
Lets extensions override even built-in tool previews.
|
||
- Made the new `PermissionGateHandler` constructor parameter **optional** so `makeHandler` and the two `external-directory-*.test.ts` handler constructions compile unchanged — only `index.ts` passes the shared registry.
|
||
Minimizes test churn.
|
||
- Open questions deferred to implementation: whether to try/catch a throwing registrant, exact MCP summary wording, and whether to record this as a formal architecture roadmap phase.
|
||
Flagged writing a disposable exploratory check against a real MCP payload before finalizing `formatMcpInputForPrompt`.
|
||
- Next step: `/tdd-plan` (this plan has red→green→commit cycles).
|
||
|
||
## Stage: Implementation — TDD (2026-05-31T21:05:00Z)
|
||
|
||
### Session summary
|
||
|
||
All five TDD steps completed across six commits (steps 1–4 plus a docs step plus a WARN fix).
|
||
Test count grew from 1628 to 1656 (+28), across 73 test files (up from 71).
|
||
Full suite, type check, lint, and `fallow dead-code` all pass.
|
||
|
||
### Observations
|
||
|
||
- **`ToolPreviewFormatter` is constructed fresh per call**, not held as instance state, so the formatter registry has to be owned by the extension factory (`index.ts`) and threaded in as an optional 4th constructor parameter on `PermissionGateHandler`.
|
||
Making it optional left `makeHandler` and the two `external-directory-*.test.ts` constructions untouched — only `index.ts` passes the real registry.
|
||
- **MCP branch bypass** was the main design surprise.
|
||
`formatAskPrompt`'s MCP early-return never called `formatToolInputForPrompt`, so the built-in MCP summarizer needed a deliberate second integration point there.
|
||
Adding `case "mcp": return "";` to the switch was also necessary — without it, when a custom formatter declines, the switch default serialises the raw MCP event to JSON and appends it to the prompt.
|
||
- **Truncation test correction**: the initial test for "truncates the full summary when it exceeds the limit" used a single 200-char string value, but `renderArgValue` caps string values at 60 chars, so the total never reached the 160-char summary limit.
|
||
Fixed by using three long-valued arguments so the joined summary exceeds 160 chars.
|
||
- `service.test.ts` had two inline `PermissionsService` literals that don't go through `makeService`; `tsc` caught them after adding the new interface method — both needed `registerToolInputFormatter: vi.fn()` added.
|
||
- **Pre-completion reviewer verdict: WARN** — one finding: `docs/architecture/architecture.md` still said "exposes two methods" after `registerToolInputFormatter` was added.
|
||
Fixed in a follow-up `docs:` commit before closing.
|
||
- Deferred (Open Questions from the plan): try/catch guard on misbehaving formatters and the exact wording of the MCP summary prefix — settled on `with key: value, ...` format which reads naturally in the prompt.
|
||
No follow-up issue needed for these; they are implementation details documented in the code.
|
||
- Post-review docs pass: a thorough authoring guide for `registerToolInputFormatter` was added to `docs/cross-extension-api.md` (commit `6d154a14`).
|
||
It covers the per-tool `input` shapes, the must-not-throw contract, `undefined`-vs-`""` semantics, the grammatical-fragment guidance, an end-to-end register/dispose lifecycle example, and recommended practices.
|
||
It also documents — and corrects an earlier misleading example about — the **MCP keying limitation**: the gate keys on the registered Pi tool name (`getToolNameFromValue`), so MCP calls all arrive under the `"mcp"` umbrella and cannot be keyed per `server:tool`; `"mcp"` is already held by the built-in.
|
||
A potential follow-up surfaced: a chained/per-`server:tool` MCP formatter model would need a richer seam than the current one-formatter-per-name registry.
|
||
|
||
## Stage: Final Retrospective (2026-06-01T00:30:00Z)
|
||
|
||
### Session summary
|
||
|
||
Shipped `#283` end to end in one continuous session: planning, TDD (9 commits), `/ship-issue` (CI green, issue closed, release-please merged to `pi-permission-system@8.3.0`), a post-ship docs-thoroughness pass, and a courtesy follow-up to the original `#266` requester.
|
||
The feature — a `registerToolInputFormatter` seam plus a built-in MCP argument summarizer — works as designed, but three of the session's friction points share one shape: the agent delivered the mechanical minimum and the user had to push for the thorough version.
|
||
|
||
### Observations
|
||
|
||
#### What went well
|
||
|
||
- Incremental verification was disciplined: `pnpm run check` plus the targeted `vitest` file ran after each TDD step, not just at the end, so type and test regressions surfaced one step at a time.
|
||
- The plan flagged "confirm the real MCP input shape before writing `formatMcpInputForPrompt`," and the agent followed through with a `grep` of `mcp-targets.ts` / `input-normalizer.ts` instead of guessing the `{ tool, server, arguments }` shape.
|
||
- `ask_user` resolved genuine design ambiguity (precedence, reference-built-in target) cleanly in two focused calls without over-asking.
|
||
- The `#266` follow-up disclosed the MCP keying limitation honestly rather than overstating the feature.
|
||
|
||
#### What caused friction (agent side)
|
||
|
||
1. `scope-drift` (user-caught) — the first-pass docs for the public seam (`docs: document tool input formatter seam`, `2fc9ff1d`) were just the interface signature plus one example.
|
||
The user had to ask "Did we thoroughly document how to create these formatters…?"
|
||
which triggered a substantial authoring guide (`6d154a14`, +115/−19) covering per-tool `input` shapes, the must-not-throw contract, `undefined`-vs-`""` semantics, lifecycle wiring, and limitations.
|
||
Impact: one extra user prompt and one follow-up commit; every gap was knowable when the thin docs were written.
|
||
2. `premature-convergence` (partly user-surfaced) — the agent committed to "MCP summarizer keyed to `mcp`" in planning without tracing the umbrella-keying constraint to its conclusion: because every MCP call arrives under the single `mcp` tool, the seam can *never* do bespoke per-MCP-tool rendering, which is literally what `#266`'s title ("smart formatters for known MCP tools") and the `ctx_batch_execute` example asked for.
|
||
The limitation only became explicit while writing the authoring guide and was disclosed post-ship.
|
||
Impact: no rework — the generic summarizer is still valuable — but the shipped feature only partially fulfills the original `#266` ask, surfaced after release.
|
||
3. `missing-context` (user-caught) — the `/ship-issue` close comment mentioned `#266`, but the agent did not proactively notify the human requester (`@kuba-4chain`) on their issue; the user prompted "We should also get back to the submitter of `#266`, right?"
|
||
Impact: one extra user prompt and one follow-up comment to close the loop.
|
||
4. `instruction-violation` (self-identified) — the `/plan-issue` prompt says "multiple `pkg:*` labels → cross-package → root `docs/plans/`," but after the user confirmed the work was `pi-permission-system`-only, the agent filed the plan in the package directory instead.
|
||
The override was correct (the determinant is which packages' code changes, not the labels), but the prompt's rule was mechanically wrong for this case.
|
||
Impact: added deliberation, no rework.
|
||
5. `other` (self-caught) — the first "truncates the full summary" test used a single 200-char value that could not exceed the 160-char summary cap, because `renderArgValue` caps each value at 60 chars; fixed in the same red step with three long-valued arguments.
|
||
Impact: negligible, caught before commit.
|
||
|
||
#### What caused friction (user side)
|
||
|
||
- Two of the friction points (docs thoroughness, notifying the `#266` submitter) were corrections the user could instead have pre-empted by stating up front "this is a public API — write authoring docs and notify the original requester on ship."
|
||
Framed as opportunity: a one-line "treat this as a third-party-facing API" cue at planning time would likely have produced the thorough docs and the courtesy follow-up without the two mid-stream nudges.
|
||
|
||
### Diagnostic details
|
||
|
||
- **Model-performance correlation** — the only subagent dispatch was the `pre-completion-reviewer` on `anthropic/claude-sonnet-4-6`, judgment-heavy review work; appropriate task-model fit, and it caught a real staleness (`architecture.md` said "exposes two methods" after a third was added).
|
||
- **Feedback-loop gap analysis** — positive: verification ran incrementally (per-step `check` + targeted `vitest`), with `lint` and `fallow dead-code` reserved for the end where their cross-cutting scope belongs.
|
||
No end-only-verification gap.
|
||
- Escalation-delay and unused-tool lenses found nothing notable (no rabbit-holes, no >5-call error loops, no obviously-skipped tool).
|
||
|
||
### Changes made
|
||
|
||
1. `AGENTS.md` (Code Style) — added a rule: public or cross-extension APIs must be documented for third-party authors (input/return contract, error/throw semantics, a minimal wiring example, known limitations), not just the type signature.
|
||
Addresses friction #1.
|
||
2. `.pi/prompts/plan-issue.md` — clarified that `pkg:*` labels are a hint, not the determinant: a plan is cross-package only if code in more than one package actually changes; a confirmed single-package scope files in that package's directory despite multiple labels.
|
||
Addresses friction #4.
|
||
|
||
Not implemented (user declined): proposal B (a `/ship-issue` step to notify an external requester when shipping work deferred from their issue, friction #3).
|
||
The behavior was still performed manually this session via the `#266` follow-up comment.
|