mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
191 lines
17 KiB
Markdown
191 lines
17 KiB
Markdown
---
|
||
issue: 737
|
||
issue_title: "pi-permission-system: decide the prompt-presentation contract — invariant core, elision rules, size bounds (ADR)"
|
||
---
|
||
|
||
# Retro: #737 — decide the prompt-presentation contract
|
||
|
||
## Stage: Planning (2026-08-14T17:14:32Z)
|
||
|
||
### Session summary
|
||
|
||
Planned ADR 0011, the prompt-presentation contract keystone (K3 from the 2026-08-12 backlog triage), which decides what a permission ask prompt must always show, what may be elided, and what bounds its size.
|
||
Read the six dependants ([#710], [#713], [#648], [#654], and PRs [#656], [#716]), traced the five prompt-assembly sites and the four consumers of the flat `message` string, and ran an `ask_user` gate that widened the ADR's scope on three axes.
|
||
The plan is documentation-only, follows the [#639]/ADR-0009 posture (survey → verify → `ask_user` gates → prose), and is committed at `packages/pi-permission-system/docs/plans/0737-prompt-presentation-contract-adr.md`.
|
||
|
||
### Observations
|
||
|
||
Operator decisions at the `ask_user` gate, all widening scope relative to the issue body:
|
||
|
||
- Deliverable is **ADR only** — no code, all six dependants stay open.
|
||
- The contract governs **four** consumers, not one: the TUI dialog, the review log, the `permissions:ui_prompt` broadcast, and the agent-facing `denial-messages.ts` text.
|
||
- A **structured payload** replacing the flat `message: string` is a live option, with its breaking implications (forwarded wire, `ui_prompt` payload) priced into the ADR rather than excluded.
|
||
- The ADR ends with a **per-item staging verdict** for all six dependants, so the follow-up `/pr-review` sessions apply a recorded decision.
|
||
|
||
Measured findings that shaped the plan (all verified against `main` this session):
|
||
|
||
- The bash branch of `formatAskPrompt` interpolates the raw command with **no cap at all**; the two configurable caps govern only the non-bash JSON/search previews.
|
||
So [#710]'s unbounded prompt was never a misconfiguration — nothing bounded it.
|
||
- A forwarded ask is assembled **twice under two configs** (child assembles, parent prefixes), so "consistent across local and forwarded asks" is structurally unattainable while the payload is a pre-assembled string.
|
||
This is the strongest argument in the option space for the structured payload (O4).
|
||
- `message` rides into the review log **unredacted** — `redactedJsonStringify` masks by key name and `message` is not a sensitive key.
|
||
Today that caps at ~200 characters of tool input; PR [#716], which removes that truncation for pretty-printed JSON, would make the review log persist unbounded unredacted input.
|
||
Neither the PR nor [ADR 0010] anticipated this interaction, and it is now a named finding the ADR must rule on.
|
||
|
||
Two facts deliberately left unverified and pushed to Build Order step 1 (with an `Explore` subagent on `sonnet-5`): whether Pi renders a pending tool call in the transcript at gate time, and whether `app.tools.expand` ([#642]) can reach anything for a forwarded ask.
|
||
The plan marks both as inferences from the wiring, not measurements, because parameter 3 (how the user reaches the full text) depends on the answer and an assumed answer would silently pick an option.
|
||
|
||
Risks carried: the [#581] transcription failure (mitigated by survey-then-gates-then-prose ordering and marking every leaning reopened), and the risk of an unenforceable-prose contract (mitigated by requiring each rule to name its conformance mechanism).
|
||
Option O6 — "no bound; the TUI's wrapping is the bug" — was added deliberately as the counter-hypothesis to [#710], so the ADR cannot ratify a content contract without first rejecting the viewport fix.
|
||
|
||
No follow-up issues filed: every deferred item already has an issue, and the plan names no new concrete work.
|
||
|
||
## Stage: Implementation — Build (2026-08-14T19:01:55Z)
|
||
|
||
### Session summary
|
||
|
||
Executed the docs-only Build Order in four commits: verified the two open facts against the sibling Pi checkout, surveyed prior art, ran three `ask_user` deliberation rounds settling all eight open parameters, authored ADR 0011, and reconciled `docs/architecture/architecture.md`.
|
||
The ADR decides a single rule — the payload is complete and elision is a property of a render, never of the payload — with an invariant `request` fact group, a row-plus-width render budget, five renderers, and a per-item staging verdict for all six dependants.
|
||
Pre-completion review returned WARN on one real gap, which was fixed, and PASS on re-review.
|
||
|
||
### Observations
|
||
|
||
Fact verification changed the design before the gates ran, which is why the plan put it first.
|
||
The host already renders the pending tool call above our dialog (`ToolExecutionComponent` is added on `message_update`, before `beforeToolCall`), it renders `$ <full command>` unbounded for `bash`, and it computes a real diff for a pending `edit` — so [#648] is partly host-provided already.
|
||
`ToolRenderContext.expanded` reaches the *call* renderer, not just the result renderer, so [#642]'s Ctrl+O genuinely expands a pending `write`/`read` and does nothing for `bash`/`edit`.
|
||
Both plan inferences about the forwarded case were confirmed: no host block exists, so the prompt is the sole evidence carrier there.
|
||
|
||
The operator's round-1 note reframed the whole ADR: rather than choosing among content rules, carry a complete payload and make elision a rendering concern.
|
||
That single move dissolved the forwarded double-assembly problem, turned [#716] into a renderer rather than a formatter edit, and made [#656]'s post-assembly truncation the wrong layer rather than the wrong number.
|
||
[#713] was promoted from enhancement to conformance requirement, corroborated by a Codex user report of an approval dialog showing only the text before `&&`.
|
||
|
||
Prior art was unusually decisive.
|
||
Codex merged "tui: fix approval dialog for large commands", moving the command preview out of the dialog into history.
|
||
Claude Code carries both "render multi-line bash args in full" and "a subagent's large inline payload froze the terminal" as open reports — the two directions of [#716] and [#710] in one product, which is the empirical case that content rules alone cannot satisfy both.
|
||
Its `Ctrl+E` explanation (on-demand, risk-labelled, disableable) is [#654]'s shape already shipped elsewhere.
|
||
|
||
Two deviations from the plan's two-commit Build Order, both recorded in commit bodies.
|
||
First, reconciling the architecture doc surfaced a contradiction the ADR had just introduced: it gave the `permissions:ui_prompt` broadcast the complete payload while the doc's own rule gives the bus the minimum needed to stay correlatable.
|
||
That was surfaced to the operator rather than papered over, narrowed to request facts, and committed separately.
|
||
The same exchange found the "Fidelity up, disclosure down" maxim genuinely ambiguous — it reads as one tradeoff dial but means two imperatives for two audiences — and replaced it.
|
||
Second, the pre-completion WARN required a fourth commit.
|
||
|
||
The WARN was worth the round.
|
||
The invariant core named only the requesting agent, so an implementer narrowing the broadcast literally would have dropped `requesterSessionId` — the correlation field [#292] added, [#610] builds on, `docs/cross-extension-api.md` documents, and `permission-events.ts` guarantees against removal without a semver-major bump.
|
||
The ADR now states that requester identity is a request fact rather than evidence, so narrowing evidence never narrows correlation.
|
||
|
||
One naming decision worth carrying forward: the never-elided group is `request`, not `core`, because it should be named for what it holds rather than for its contract.
|
||
No follow-up issues filed — all six dependants already have numbers, and the ADR's staging table records what each becomes.
|
||
The seam itself has no issue yet: following the ADR 0007 precedent, the staging section defers its decomposition to the next `/plan-improvements pi-permission-system` pass, which files the concrete issues and sequences this work against the [#639] and [#686] keystones.
|
||
|
||
[#686]: https://github.com/gotgenes/pi-packages/issues/686
|
||
|
||
[#292]: https://github.com/gotgenes/pi-packages/issues/292
|
||
[#581]: https://github.com/gotgenes/pi-packages/issues/581
|
||
[#610]: https://github.com/gotgenes/pi-packages/issues/610
|
||
[#635]: https://github.com/gotgenes/pi-packages/issues/635
|
||
[#639]: https://github.com/gotgenes/pi-packages/issues/639
|
||
[#642]: https://github.com/gotgenes/pi-packages/issues/642
|
||
[#648]: https://github.com/gotgenes/pi-packages/issues/648
|
||
[#654]: https://github.com/gotgenes/pi-packages/issues/654
|
||
[#656]: https://github.com/gotgenes/pi-packages/pull/656
|
||
[#710]: https://github.com/gotgenes/pi-packages/issues/710
|
||
[#713]: https://github.com/gotgenes/pi-packages/issues/713
|
||
[#716]: https://github.com/gotgenes/pi-packages/pull/716
|
||
|
||
## Stage: Final Retrospective (2026-08-14T21:19:03Z)
|
||
|
||
### Session summary
|
||
|
||
All four lifecycle stages — planning, build, ship, and this retrospective — ran in a **single** session (131 assistant turns), rather than the multi-session flow `AGENTS.md` describes.
|
||
The deliverable is ADR 0011, recording the prompt-presentation contract in five commits, shipped as `b182a992` with CI green and no release (every commit lands on a release-please `exclude-paths` directory).
|
||
Two defects were introduced and caught inside the session — one by the plan's own reconcile step, one by the pre-completion reviewer — and both share a root cause worth naming.
|
||
|
||
### Observations
|
||
|
||
#### What went well
|
||
|
||
The pre-completion reviewer earned its keep on a **docs-only** deliverable, which is novel.
|
||
It did not return formatting nits; it found that the ADR's invariant core named only the requesting agent, so an implementer narrowing the broadcast literally would have dropped `requesterSessionId` — a field [#292] added, [#610] builds on, `docs/cross-extension-api.md` documents, and `permission-events.ts` guarantees against removal without a semver-major bump.
|
||
That is a decision-record defect a human reviewer would plausibly have missed, on a change with no code to test.
|
||
|
||
Verifying the `Explore` subagent's universal claim changed the ADR's evidence base.
|
||
The subagent reported that `setToolsExpanded` affects "only COMPLETED tool results, not pending calls", with citations.
|
||
A direct read of `../pi` found `getRenderContext` passes `expanded: this.expanded` into the **call** renderer too (`tool-execution.ts:115-133`, invoked at `:275`), and `read.ts:338` consumes it — so [#642]'s Ctrl+O genuinely expands a pending `write`/`read`.
|
||
Had the claim been trusted, the ADR's full-text-access rule would have been written against a false constraint.
|
||
This is `AGENTS.md`'s "a subagent's universal claim is the one to verify" paying off concretely.
|
||
|
||
The prior-art survey turned an opinion into evidence.
|
||
Claude Code carries **both** "render multi-line bash args in full" and "a subagent's large inline payload froze the terminal" as open reports — the two directions of [#716] and [#710] in one product — which is the empirical case that content rules alone cannot satisfy both.
|
||
Codex's merged "tui: fix approval dialog for large commands" supplied a third option (O7) that the plan's option space did not contain.
|
||
|
||
Model allocation across stages was well matched: `claude-opus-5` for the judgment-heavy planning and ADR deliberation (turns 1–104), `claude-sonnet-5` for the mechanical ship flow (turns 105–125), `claude-opus-5` again for this retrospective (turns 126–131).
|
||
|
||
#### What caused friction (agent side)
|
||
|
||
- `missing-context` — the ADR's §6 gave the `permissions:ui_prompt` broadcast the complete payload, contradicting `architecture.md:534`'s rule that the bus "receives the minimum needed to stay correlatable, because any loaded extension can observe it".
|
||
The plan had **already listed that exact passage** ("the cross-extension broadcast paragraph (line 534)") in its Module-Level Changes as a candidate to reconcile; the authoring step did not consult its own list.
|
||
Impact: a contradiction shipped into `5c47c211`, caught at Build Order step 5, requiring commit `4d14b75c` plus three `ask_user` rounds with the operator.
|
||
|
||
- `missing-context` — narrowing the broadcast in `4d14b75c` did not enumerate what the broadcast currently carries, so `requesterSessionId` went unmentioned.
|
||
Impact: one extra commit (`be7973bf`) after the reviewer's WARN.
|
||
Same root cause as the item above: **a contract was decided without first enumerating its current fields and their guarantees.**
|
||
|
||
- `instruction-violation` (user-caught) — dense context was packed into `ask_user` option descriptions instead of the message preceding the call.
|
||
The operator bounced two gates: once asking for prose first ("Give me deeper explanation here.
|
||
Don't pack it all in to an ask_user call") and once for concrete artifacts ("Please show me some examples of the different payloads").
|
||
`.pi/prompts/plan-issue.md:103` states this rule (Refs [#635]), but the violated gates ran under `/build-plan`, whose prompt contains **zero** `ask_user` guidance.
|
||
Impact: two extra deliberation rounds; no rework to the artifact.
|
||
|
||
- `missing-context` — ship stage: queried the per-package block of `release-please-config.json` for `exclude-paths` and got `[]`, when the key is top-level.
|
||
Impact: one extra tool call, self-identified immediately, no rework.
|
||
|
||
#### What caused friction (user side)
|
||
|
||
No friction.
|
||
Two operator interventions were decisive rather than corrective:
|
||
|
||
- The round-1 note ("there should be a core structure sent, with the full set of information — it is the presentation or view or render layer which decides how that information is rendered") reframed the ADR from *choosing among content rules* to *complete payload plus bounded render*.
|
||
No offered option said that; the free-text note carried it.
|
||
This is a case for keeping `ask_user` options open-ended enough that a reframe can arrive alongside a selection.
|
||
- "What does 'fidelity up' and 'disclosure down' mean?
|
||
Which way is up and down?"
|
||
was a redirecting question, not a correction, and it surfaced that a maxim in `architecture.md` had been ambiguous since it was written.
|
||
|
||
### Diagnostic details
|
||
|
||
- **Model-performance correlation** — main session as above.
|
||
The `Explore` subagent ran on `sonnet-5` (explicitly requested per `AGENTS.md`'s multi-hop-trace guidance) for a 79-tool-use trace of Pi internals; appropriate.
|
||
Both `pre-completion-reviewer` dispatches ran on `anthropic/claude-sonnet-5` per the agent's frontmatter; appropriate for a judgment-bearing review that found a real gap.
|
||
No mismatch found.
|
||
|
||
- **`read_session` phantom model switches** — `.pi/prompts/retro.md:99` states that `[model change]` lines "are suppressed unless the switch actually ran a turn … no manual phantom-filtering is needed".
|
||
That holds only for an **unfiltered** call.
|
||
A `types`-filtered call bypasses the suppression: this session's filtered call rendered six switches, of which three (`opencode-go/deepseek-v4-flash`, `anthropic/claude-fable-5`, `anthropic/claude-haiku-4-5`, all within two seconds at `21:10:56`–`21:10:58`) never ran a turn.
|
||
An unfiltered call rendered exactly one marker, correctly suppressing all three.
|
||
Trusting the prompt's assurance would have produced a false finding that the session ran on three models it never used.
|
||
|
||
- **`read_session` cannot reach early stages of a long session** — with all four lifecycle stages in one 131-turn session, `limit: 44` returned only the ship tail plus the retro, and there is no `offset` parameter.
|
||
Whole-session model attribution required parsing the raw `.jsonl` with a `python3` script.
|
||
This is a `pi-session-tools` capability gap, recorded below as a follow-up rather than fixed here.
|
||
|
||
- **Escalation-delay tracking** — no sequence exceeded the five-call threshold.
|
||
The longest same-topic run was the four-call `read_session` investigation above, which changed approach (to raw `.jsonl`) on the third call.
|
||
|
||
- **Feedback-loop gap analysis** — nothing notable; verification was incremental rather than end-loaded (`pnpm run check` + `pnpm run lint` at baseline, `rumdl check` on each file before its commit, `pnpm run lint` after each of the five commits).
|
||
|
||
### Follow-ups
|
||
|
||
1. `read_session` (in `pi-session-tools`) has no `offset`/`from` parameter, so a long single session's early turns are unreachable through the tool.
|
||
Worth filing against `pi-session-tools`; not implemented here (retro scope discipline).
|
||
|
||
### Changes made
|
||
|
||
1. `AGENTS.md` — new `### Clarification gates` subsection under `## Workflow`: present the substance in a message first, then call `ask_user` with options that reference it.
|
||
Generalized from the operator's framing, which is broader than the [#635] rule it replaces (that rule covered only behavior-change differentiators).
|
||
2. `.pi/prompts/plan-issue.md:103` — shortened the [#635] copy to keep the planning-specific clause and point at `AGENTS.md` § Clarification gates, removing the duplication.
|
||
3. `.pi/prompts/retro.md:99-100` — corrected the model-attribution instruction: attribute from an **unfiltered** `read_session` call, because a `types: ["model_change"]` filter bypasses the suppression and renders phantom switches.
|
||
4. `.pi/prompts/build-plan.md:99-100` — added the contract-enumeration rule: list a published contract's current fields and stability guarantees before a decision record narrows or replaces it.
|
||
|
||
[ADR 0010]: ../decisions/0010-permission-log-secret-exposure.md
|