18 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 710 | pi-permission-system: Forwarded subagent permission prompts render unbounded tool input inline and push the parent transcript out of view |
Retro: #710 — Bounded local renderers for the permission dialog
Stage: Planning (2026-08-15T07:18:11Z)
Session summary
Planned Phase 13 Step 2: src/presentation/dialog-renderer.ts rendering the landed PromptPayload under a row budget plus a per-field width cap, wired into the inline TUI dialog and the select/input fallback, with Ctrl+O toggling the complete view.
Nine TDD cycles; batch "presentation-payload" tail, so this issue's fix: is the release vehicle for Step 1 (#744) as well.
Plan committed at packages/pi-permission-system/docs/plans/0710-bounded-dialog-renderer.md.
Observations
- The issue is third-party (
aoguai), but the direction was already settled. ADR 0011 adopted #710 as "fixed by construction" and the Phase 13 roadmap assigns it to Step 2, so theask_usergate spent its budget on design parameters rather than on whether to build it. - Measured, not estimated.
A disposable spike over the real
wrapTextWithAnsiput the reported case at 202 rows local / 205 rows forwarded for a 200-line here-string (10 236 chars), identical at widths 80/120/160 — the here-string carries its own newlines, so a wider terminal buys nothing. That number is the plan's baseline and becomes a regression assertion. - ADR 0011 §3 and §5 only cohere under one reading, and this plan states it.
§3 says no budget may elide the
requestcore; §5 justifies the width cap by "a here-string on one logical line" — which in this very report isrequest.value. Operator confirmed: "never elided" means never omitted, so a core fact always keeps its labelled line while its text may be shortened and reached in full. Under the alternative reading the reported ask still costs 86–202 rows and #710 is not fixed, so this is load-bearing and goes into the architecture doc. - The row budget bounds evidence; the field cap bounds the core.
Stating the precedence explicitly (§3 outranks §5 when a capped core alone exceeds
maxRows) avoided a shrink-to-fit algorithm that would have been fiddly to test and impossible to explain. Ctrl+Oreuse over a new key.handleToolsExpandActionalready interceptsapp.tools.expandfor the host forward (#642); it gains the dialog's own toggle so "expand" means one thing in both places, and the #642 forward assertion is extended in the same cycle that adds the toggle.- PR #738's
highlightTextfield is redundant under the payload. The flagged element is derivable fromrequest.value(or theexternal pathevidence for the bash external-directory kind), which removes the "highlight target diverges from rendered text" risk the PR guarded with tests. Both PR #738 and PR #716 close as superseded at ship, withCo-authored-bycredit in the relevant cycles. - Config defaults chosen roomy:
promptMaxRows24,promptFieldMaxWidth400. The field cap does the work for the reported case (400 chars ≈ 4 rows at width 100); the row budget mostly bounds evidence. - Rejected: an expansion affordance in the
select/inputfallback. ADR 0011 §6 records that renderer as assuming none and aselecthas no keystroke channel; recorded as rationale in Open Questions rather than filed as a follow-up. - No follow-up issues filed. Every deferral this plan names already has an issue — #745 (wire + broadcast + preview-cap soft-deprecation), #746 (agent + review-log renderers), #654 (annotations), #519 (RPC/frontend prompt surface).
Stage: Implementation — TDD (2026-08-15T07:57:35Z)
Session summary
Landed Phase 13 Step 2 in 15 commits: three tidy-first preparatory commits, nine planned cycles, and two commits answering the pre-completion review.
The inline dialog and the select/input fallback now render the structured payload through src/presentation/dialog-renderer.ts under a row budget plus a per-field width cap, with Ctrl+O expanding to the complete request.
Test count 2944 → 2978 (+34); check, root lint (0 findings), fallow dead-code, and verify:public-types all clean.
Observations
- The field cap, not the row budget, is what fixes #710.
The repro test passed the moment the per-field cap landed (cycle 3), before the row bound existed: the here-string is
request.value, so capping it took the render from 205 rows to about 11. The row budget bounds the evidence — which is exactly the division of labour the plan predicted, but it was worth seeing the cycle-3 test go green to know which mechanism carries the fix. - The dedup rule surfaced a test expectation that was wrong, not code that was.
Cycle 1's
path-ask expectation asserted asurface : pathline; the renderer dropped it because the value line's own label already sayspath. The renderer was right and the expectation was fixed — but the pre-completion reviewer then correctly flagged that this omission is a second mechanism by which a core line can be absent, distinct from the width cap the docs described. Botharchitecture.mdanddocs/configuration.mdnow state the redundancy rule beside the never-omitted rule so neither reads as the other. Ctrl+Oneeded no change to the host forward. The toggle went intohandleInputat the one place that both knows the component and already treats the keystroke as handled, sohandleToolsExpandActionis untouched and #642's invariant holds by construction rather than by care.- Plan deviations, all small:
the
Paintseam shipped asHighlightPaint = (text: string) => stringrather than the plan's(role, text)— no caller ever needed a"label"role, and an unused parameter would have been speculative; the seam was introduced in cycle 5 (where it is used) rather than cycle 1 (where it would have been dead);authorizer.ts,permission-dialog.test.ts,config-schema.test.ts, andconfig-reporter.test.tswere listed in Module-Level Changes but needed no edit; andconfig-pipeline.test.tswas edited but not listed — it is the strongest place to pin the #332/#347 config-drop class, which the plan named as a risk without naming its test. - The tidy-first assessor found two
PromptPreferencesconstruction sites the plan's own grep obligation missed (both inline inlocal-user-authorizer.test.ts), which is the same class of miss #744's retro recorded. Landing the shared fixture first turned cycle 7's widening into a one-line change; thePermissionPromptViewcast removal turned a would-be runtime surprise into a compile error. - One self-inflicted friction point: an
Editcall used a fabricated absolute path with a doubled package segment, which this package's ownexternal_directorygate blocked with a message naming the correct path. Same mistake the #744 session recorded — the gate caught it both times. - Pre-completion reviewer: WARN (no FAILs).
Both findings were addressed rather than deferred: the documentation-precision nit on the redundancy rule, and a local variable in
coreFactsnamedvaluewhile holding a label. Its third note (the narrowerPainttype, the unlisted test file) is recorded above as a deviation.
Stage: Final Retrospective (2026-08-15T16:19:51Z)
Session summary
One session carried planning, TDD implementation, ship, and this retrospective for Phase 13 Step 2: the bounded permission-dialog renderer.
Seventeen commits landed pi-permission-system@25.3.0, closing #710 and #713 and superseding PRs #716 and #738 with authorship credited.
The single user intervention of the session — a question about commit typing — exposed a changelog-honesty problem that cost a six-commit history rewrite and uncovered a silent co-authorship defect.
Observations
What went well
- A planning-time measurement drove a design decision, not just a plan sentence.
A disposable vitest spike over the real
wrapTextWithAnsiput the reported ask at 202 rows locally / 205 forwarded. That number then did five jobs: it proved ADR 0011 §3 and §5 only cohere under one reading (the field cap must apply to the core, or the reported ask stays at 86–202 rows), it grounded theask_useroption set, it became the plan's predicted-effect table, it became a regression assertion intest/presentation/dialog-renderer.test.ts, and it became the evidence in the issue close comment. The/plan-issuemeasurement rule exists to avoid false precision; here the measurement changed the design rather than decorating it. - The tidy-first assessor caught the residue of a rule added one session earlier.
#744's retro added a
/plan-issuegrep obligation for a newly required interface field (grep constructors, not use sites). This plan followed it and still missed two inlinePromptPreferencesconstructions inlocal-user-authorizer.test.ts. The assessor found both, plus theas unknown as PermissionPromptViewcasts that would have let a missingbudgetfield compile clean. A rule plus a fresh-context backstop caught what the rule alone did not. - TDD ordering produced a diagnostic the plan could not. The #710 repro assertion went green at cycle 3, before the row bound existed — proving the field cap is what fixes the reported case and the row budget only bounds evidence. The plan predicted that division of labour; the cycle order demonstrated it.
- Tree-identity verification made a six-commit history rewrite safe.
Every rebase pass was checked with
git diff --stat pre-retype-710 HEADagainst a backup tag, not by reading the rebase's own output — which is exactly what caught the silent no-op below.
What caused friction (agent side)
missing-context(user-caught) — five cycles that built a module nothing imported yet were typedfeat:, and the wiring commit that changed every user's prompt appearance was typedfix:. The precedent was already in a document read during planning: #744's retro records Step 1 as an "all-hidden commit range" for exactly this situation. The plan then propagated the wrong types into its TDD Order, and implementation followed the plan faithfully. Impact: a user correction, a six-commitGIT_SEQUENCE_EDITORrewrite, and roughly 15 tool calls. The published changelog would otherwise have read as a construction diary — seven feature lines including two near-identical "bound the …" entries describing an internal seam — with the appearance change filed under Bug Fixes.other(self-identified) — the first scripted rebase reportedSuccessfully rebased and updated refs/heads/mainwhile changing not one subject. This git writes its todo aspick <sha> # <subject>; the sequence-editor pattern expected no#, so every line stayedpickand the rebase replayed as a no-op. Impact: three diagnostic tool calls (dry run on a fake todo,git configcheck, dumping the real todo) plus a re-run. Caught by diffing the subjects afterwards, not by the rebase's exit message — the same class asAGENTS.md'stail-masking trap, where the status comes from the wrong thing.other(self-identified) —Co-authored-by:was written above theRefs #710, #716paragraph, so git's trailer parser saw no trailer block at all and GitHub would not have attributed either contributor.Refs #710, #716has no colon, so it is not trailer-shaped, and it was the final paragraph. Verified both ways withgit interpret-trailers --parse: empty for the shipped ordering, correct forRefsfirst. Impact: one more rebase pass (three tool calls) — but the real cost was a false claim, since the turn-200 summary had already told the operator credit was given. This is a direct collision withAGENTS.md's own house style, which putsRefs #Nlast.instruction-violation(self-identified) — aneslint-disable-next-line @typescript-eslint/prefer-nullish-coalescingwas added preemptively inpermission-prompt-component.ts; the rule never fired, so lint rejected the unused directive. Thecode-designskill states the rule plainly: add a disable only after the linter reports it. Impact: one lint failure, one removal edit, one re-run. No new rule warranted — the existing one is correct and the root-level lint caught it inside the same cycle.instruction-violation(self-identified) — oneEditused a fabricated absolute path with a doubled package segment (…/pi/pi-permission-system/test/…instead of…/pi/pi-packages/packages/pi-permission-system/test/…). Impact: one denied call, corrected immediately. Second consecutive session with this exact error (#744's retro records the first). No rule proposed: this package's ownexternal_directorygate blocked it and named the correct path, which is the backstop working as designed.instruction-violation(self-identified) — this retro entry re-added[#710]:and[ADR 0011]:link definitions that the planning stage had already defined, trippingMD053. Themarkdown-conventionsskill names this exact case: link reference definitions are file-scoped, so an appended stage references them without redefining. Impact: onerumdlfailure and one removal edit, caught before the commit.
What caused friction (user side)
- None. The session's one intervention was a redirecting question ("Shouldn't a fresh presentation to the user at least warrant a feat?") rather than a correction, and it was strictly better than a correction would have been: it surfaced the literal mis-typing and, on investigation, the larger diary-changelog problem the question did not name.
- One structural opportunity, not a user failure: the commit types were visible in the plan's TDD Order at plan-review time, but a list of nine
feat:/fix:subjects is not legible as "what the changelog will say". Nothing in the workflow renders that view, so there was nothing cheap for the operator to react to until the commits existed.
Diagnostic details
- Model-performance correlation — planning, TDD, the commit-retype, and this retrospective ran on
anthropic/claude-opus-5; the ship stage ran onanthropic/claude-sonnet-5. Both subagents (tidy-first-assessor,pre-completion-reviewer) rananthropic/claude-sonnet-5per their frontmatter. The split is appropriate and matches #744: shipping is a deterministic checklist, while planning and TDD carried the design judgment. No mismatch in either direction — both subagents did judgment-heavy work well, the assessor finding fixture sites the plan's grep missed and the reviewer catching a documentation-precision gap about a second omission mechanism. - Escalation-delay tracking — no
rabbit-holefriction points. The longest single-error sequence was three tool calls (diagnosing the rebase no-op), well under the five-call threshold. - Unused-tool detection — nothing missed.
colgrepwent unused, correctly: every search was exact-symbol (formatAskPrompt,doublePressToConfirm,requestPermissionDecision), which thecolgrepskill's decision table assigns togrep. - Feedback-loop gap analysis — no gap.
pnpm run checkplus the cycle-scopedvitest run <file>ran after every red and green; rootpnpm run lintran before every commit, which is what caught the speculativeeslint-disableinside its own cycle;pnpm run test,pnpm fallow dead-code, andverify:public-typesran at the end and again after the retype.
Changes made
AGENTS.md§ Commits — type a commit by what a user can observe once it lands, not by what it adds to the tree; a module nothing imports yet isrefactor:and the wiring commit carries thefeat:/fix:.AGENTS.md§ Commits —Co-authored-by:belongs in the final paragraph, belowRefs #N, because git reads only the last paragraph as trailers andRefs #Nis not trailer-shaped; verify withgit interpret-trailers --parse.AGENTS.md§ Commits — a scripted rebase reportsSuccessfully rebasedeven when its sequence editor matched nothing, since this git writes its todo aspick <sha> # <subject>; verify by diffing subjects and confirm content withgit diff <backup-tag> HEAD..pi/prompts/tdd-plan.md— added a changelog-preview check to "After the last TDD step" (new item 9), so a commit describing an internal seam is retyped before anything is pushed.
Four candidates were considered and declined, recorded so a later session does not re-derive them:
- A rule for the doubled-package-segment
Editpath (second consecutive session) — theexternal_directorygate blocks it and names the correct path, so the backstop already works. - Added emphasis on
code-design's speculative-eslint-disablerule — the rule is adequate and root lint caught the violation inside its own cycle. - A
markdown-conventionschange for duplicate link-reference definitions — the skill already names this exact case andrumdlcaught it pre-commit. - Putting the changelog preview in
/plan-issueinstead of/tdd-plan— cheaper to act on, but the plan is a prediction, and this session's prediction was the thing that was wrong.