8.6 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 282 | Extract ToolPreviewFormatter from tool-input-preview.ts |
Retro: #282 — Extract ToolPreviewFormatter from tool-input-preview.ts
Stage: Planning (2026-05-30T18:00:00Z)
Session summary
Produced a numbered implementation plan for extracting a ToolPreviewFormatter class from the flat tool-input-preview.ts module and threading it through the gate descriptor chain.
The plan covers 6 TDD cycles: extract the class, thread through describeToolGate/formatAskPrompt, wire construction in PermissionGateHandler, remove the module-level vi.mock in permission-prompts.test.ts, and update architecture docs.
Referenced the Phase 1 roadmap in the architecture doc and confirmed #285 (handleToolCall decomposition) is already completed.
Observations
- The architecture doc's roadmap was comprehensive and directly translatable to a concrete implementation plan.
The dependency ordering (#285 before Phase 1 step 2) was verified correct by checking the current code —
permission-gate-handler.tsalready has the decomposed pipeline. - The existing
tool.tsgate test (test/handlers/gates/tool.test.ts) andpermission-prompts.test.tsboth need formatter injection but in different ways:tool.test.tsneeds a real formatter instance fordescribeToolGate;permission-prompts.test.tsneeds to replace its module-level mock with direct injection. - The
permission-prompts.test.tsmock removal is not purely mechanical — tests that asserttoHaveBeenCalledWithon the mockedformatToolInputForPromptneed rework to assert on the real result string. The plan calls this out explicitly in step 5. - Included
toolInputLogPreviewMaxLengthinToolPreviewFormatterOptionseven though the issue only lists two fields, because log-formatting methods (formatGenericToolInputForLog,getToolInputPreviewForLog,getPermissionLogContext) use it and they're all moving to the class. If #266 decides not to expose it in config, the field defaults to 1000 and remains internal. - No ambiguity worth asking the user about — the issue proposed clear steps.
Stage: Implementation — TDD (2026-05-30T22:30:00Z)
Session summary
Extracted ToolPreviewFormatter from tool-input-preview.ts and threaded it through the gate descriptor chain in 4 commits (test step 1, refactor steps 2–5 combined, style fix, docs).
All 68 test files pass with 1527 tests, a net gain of 7 tests over the 1520 baseline.
The vi.mock in permission-prompts.test.ts was removed; the formatter is now injected directly.
Observations
- Plan deviation — steps 2–5 folded into one commit.
Removing the 7 config-dependent exports from
tool-input-preview.tsimmediately broketool.ts,permission-prompts.ts, and their tests at the TypeScript level, making it impossible to commit the extraction without simultaneously updating all consumers. The intermediate state was uncompilable, so the extraction, threading, test updates, andvi.mockremoval all landed in one refactor commit. Noted in the commit body. - ESLint
prefer-nullish-coalescinginsanitizeInlineText. ThemaxLength !== undefined ? maxLength : defaultternary intool-preview-formatter.tswas caught by the pre-commit hook; fixed before committing by collapsing tomaxLength ?? this.options.toolTextSummaryMaxLength. - Biome
useTemplatewarnings. Two string-concatenation lints intool-preview-formatter.test.tsrequired a manual edit (unsafe auto-fix); patched with a separatestyle:commit. - Pre-completion reviewer WARNs (intentional):
formatAskPromptaccepts the fullToolPreviewFormatterrather than a narrower{ formatToolInputForPrompt }interface — documented in the plan as intentional for forward compatibility.formatAskPromptsilently returns empty preview whenformatterisundefined— documented in the plan as safe default behavior.
- Pre-completion reviewer verdict: PASS.
Stage: Final Retrospective (2026-05-31T02:49:40Z)
Session summary
Shipped issue #282 cleanly: synced, ran root-level pnpm run lint and pnpm fallow dead-code, pushed, watched CI to success, and closed the issue with an implementation summary.
No release-please PR appeared because the change is a refactor: with no feat:/fix: commits — these changes will release with the next semantic commit to pi-permission-system.
This retrospective spans all three stages (Planning, TDD, Ship).
Observations
What went well
- The ship stage was friction-free: every gate (
lint,fallow dead-code, CI, issue close) passed on the first attempt. - Incremental verification during TDD was strong —
pnpm run checkran immediately after the export-removal edit and surfaced the three broken consumers (tool.ts,permission-prompts.ts, and their tests) at once, which is what made the steps 2–5 fold an obvious, deliberate decision rather than a surprise. - The
pre-completion-reviewersubagent caught the twoformatAskPromptdesign WARNs and correctly classified them as intentional-per-plan, so no churn resulted.
What caused friction (agent side)
missing-context— during Planning, the plan file added a[#282]:reference-link definition for the plan's own issue number, but the body never links to[#282](a plan does not reference itself). This tripped markdownlint MD053 (unused reference) and was not caught until the TDD baseline ranpnpm run lint, forcing a fixup commit (b4c4b52a docs: fix unused link reference in plan 0282). The pre-commit hook runsrumdl fmt(formatting) but notrumdl check(linting), so the Planning commit passed its hook with the latent failure. This is the second occurrence of link-reference-definition trouble in adjacent sessions — #285 needed1e05657e docs(retro): remove duplicate link reference definitions in retro file. Impact: one fixup commit per session; user-caught risk avoided only because the next stage happened to lint.wrong-abstraction— during TDD, a multi-edit ontool-input-preview.tsremovedgetNonEmptyStringfrom the top-level import and replacedgetPromptPath's body with an inlinerequire("./common")call instead of simply keeping the import. Self-identified immediately by reading the file after the edit; fixed in two follow-up edits before any commit. Impact: ~2 extra edits, no commit churn.missing-context(minor) — during Planning, the agent tried to read the colgrep skill at.pi/skills/colgrep/SKILL.mdand gotENOENT; the skill actually lives atpackages/pi-colgrep/skills/colgrep/SKILL.md. Most package skills sit under.pi/skills/, so the guessed path was a reasonable but wrong default. Impact: one failed read, no rework.
What caused friction (plan side)
premature-convergence— the plan split the extraction (step 2, "pure extraction… not yet used by any consumer") from the consumer threading (step 4), but removing the seven exports fromtool-input-preview.tsbreaks every importer at the type level in the same commit, so the split was not buildable. The existingplan-issue.mdrule covers "an export that has a single call site (e.g.,index.ts)" — it does not generalize to an export with multiple consumers plus their test files. Impact: no rework (TDD folded steps 2–5 and noted the deviation), but the six-step structure was misleading and required a deviation note in the commit body and TDD retro.
Diagnostic details
- Feedback-loop gap analysis — the Planning stage commits the plan without running
pnpm run lint:md; the only markdown gate at commit time is the pre-commitrumdl fmt, which formats but does not flag MD053. The unused link reference therefore survived until the TDD baseline lint. Addressed indirectly by the markdown-conventions rule below (cheaper than adding a lint step to the Planning prompt). - Model-performance correlation — the only subagent across all stages was the TDD-stage
pre-completion-reviewer(judgment-heavy review); appropriate match, no mismatch. - Escalation-delay / unused-tool — no rabbit-holes; no error sequence exceeded two tool calls; no missing subagent dispatch.
Changes made
.pi/skills/markdown-conventions/SKILL.md— extended the reference-style links bullet with a link-reference hygiene sub-rule: every[#N]:definition needs a matching[#N]body reference (MD053), and do not define a link for the doc's own issue number..pi/prompts/plan-issue.md— broadened the TDD Order export-removal rule from "single call site" to any export removal, folding the extraction plus all consumer and consumer-test updates into one step regardless of call-site count.