docs(pi): correct dual-mode gate eval — hasUI polarity, info/freetext wiring, citations

- #1 hasUI: real 0.80.3 comment is "true in TUI and RPC modes" (doc had the old
  0.73.1 "false in print/RPC"). hasUI no longer distinguishes TUI from RPC, so it
  is NOT a fallback for the interactive branch — only ctx.mode === "tui" is.
- #2 info/freetext: buildInfoRequest exists in builders.js but is NOT wired into
  tht-gate.js (imports only select/multiselect/artifact-gate). info notices are
  hand-written ctx.ui.notify; freetext is a control, not a descriptor. Table now
  lists only the 3 real present() descriptors.
- #3 cite the REWRITE banner (tht-gate.js:8-10), not :227; id-match quoted verbatim.
- #4 new "Note implementative": 227 comment, 4/6 unguarded notify, and the
  ctx.hasUI guard migration side-effect (now fires in RPC on 0.80.3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 14:13:26 +02:00
co-authored by Claude Opus 4.8
parent be02e96f9e
commit 0afa9a2f80
@@ -4,8 +4,9 @@ Date: 2026-07-04
Status: valutazione (nessuna implementazione — solo analisi di fattibilità)
Pi runtime di riferimento: `@earendil-works/pi-coding-agent` **0.80.3** — versione live sul
PATH dal 2026-07-04 (migrazione dal vecchio scope `@mariozechner`, congelato a 0.73.1;
validata via `model-matrix` con GLM 5.2, scenari `new`+`resume` entrambi CHAINED). Tutte le
citazioni `types.d.ts` sotto sono relative a **questa** versione.
validata via `model-matrix` con GLM 5.2, scenari `new`+`resume` entrambi CHAINED, e uno smoke
full-stack con gate F1 risposto). Tutte le citazioni `types.d.ts` sotto sono relative a
**questa** versione.
## Context
@@ -19,8 +20,10 @@ In RPC il backend fa da bridge (`extension_ui_request` method `input` → SSE) e
frontend React rende i descrittori come widget veri. In interattivo, invece,
`ctx.ui.input` mostra il **JSON grezzo** come prompt e pretende una risposta
`UiResponse` in JSON digitata a mano → non usabile. Il design ha volutamente
sostituito i widget TUI nativi con l'emissione descriptor
(commento in testa a [tht-gate.js:227](../../../harness/.pi/extensions/tht-gate.js)).
sostituito i widget TUI nativi con l'emissione descriptor — banner REWRITE punto (2),
[tht-gate.js:8-10](../../../harness/.pi/extensions/tht-gate.js): *"the reviewer interaction is
emitted as a widget-descriptor JSON … instead of rendered by blocking native TUI primitives
(ctx.ui.select/custom)"*.
## Verdetto: SÌ, è pensabile ed è architetturalmente supportato
@@ -29,12 +32,14 @@ sfruttata:
- **`ctx.mode: ExtensionMode`** (types.d.ts:207,212) — `"tui" | "rpc" | "json" | "print"`,
con commento dei tipi: *"Use 'tui' to guard terminal-only UI such as custom components"*.
È **il** discriminatore corretto per il ramo interattivo: guardare `ctx.mode === "tui"`,
perché con 4 modi `!ctx.hasUI` confonderebbe `rpc`/`json`/`print` in un solo ramo.
- **`ctx.hasUI: boolean`** (types.d.ts:214) — "Whether UI is available (false in print/RPC
mode)"; resta disponibile e **il gate lo usa già**
([tht-gate.js:319,397](../../../harness/.pi/extensions/tht-gate.js)). Valido come fallback,
ma `ctx.mode === "tui"` è più preciso ora che i modi sono quattro.
È **il** discriminatore corretto per il ramo interattivo: guardare `ctx.mode === "tui"`.
- **`ctx.hasUI: boolean`** (types.d.ts:213) — commento reale: *"Whether dialog-capable UI is
available (**true in TUI and RPC modes**)"*. ⚠️ È `true` **sia** in TUI **sia** in RPC:
quindi **non** distingue interattivo da RPC e **non** è un fallback valido per il ramo TUI —
solo `ctx.mode === "tui"` lo fa. (Nota migrazione: il gate usa oggi `if (ctx.hasUI)` a
[tht-gate.js:319,397](../../../harness/.pi/extensions/tht-gate.js) con la vecchia semantica
0.73.1, dove `hasUI` era `false` in RPC. Su 0.80.3 quelle due guardie scattano **anche** in
RPC → vedi *Note implementative*.)
- **`ExtensionUIContext`** (types.d.ts:63-67) — "Each mode (interactive, RPC, print)
provides its own implementation". Espone nativamente:
`select(title, options: string[], opts?) → Promise<string|undefined>` (69),
@@ -42,7 +47,7 @@ sfruttata:
`input(title, placeholder?, opts?) → Promise<string|undefined>` (73),
`notify(message, type?)` (75), `editor(title, prefill?) → Promise<string|undefined>`
(134), e `custom<T>(factory) → Promise<T>` (116, componente TUI a pieno controllo).
`info`/`notify` funzionano già in entrambe le modalità.
`notify` funziona già in entrambe le modalità.
## `UiResponse` è un tipo INTERNO ThothII (non un export di Pi)
@@ -67,7 +72,7 @@ descriptor. Va documentato esplicitamente (JSDoc typedef nel modulo del gate):
L'eleganza sta nel fatto che a valle tutto consuma un `UiResponse`, classificato da
`resolveSelectOutcome` / `resolveConfirmOutcome` / `selectedChoice`, con l'invariante
id-match in `emitAndWait` ([tht-gate.js:312](../../../harness/.pi/extensions/tht-gate.js):
`resp.id === descriptor.id && resp.control !== "cancel"`). Quindi:
`if (resp && resp.control !== "cancel" && resp.id === descriptor.id)`). Quindi:
- **Invariati**: `gate/builders.js` (costruzione descrittori) e i classificatori di
esito. Sono il contratto condiviso tra le due modalità.
@@ -80,17 +85,25 @@ id-match in `emitAndWait` ([tht-gate.js:312](../../../harness/.pi/extensions/tht
primitive native e **sintetizza** un `UiResponse` con lo stesso `id` e la stessa
forma, così i classificatori a valle non cambiano di una riga.
I 3 gate bloccanti + freetext + info sono una superficie limitata. La colonna
**no-limbo** dice cosa sintetizzare quando la primitiva nativa ritorna `undefined`
(Esc / cancel / timeout / `signal`), perché senza risposta il turno resterebbe appeso:
I **3 gate bloccanti** che passano da `emitAndWait` sono l'intera superficie da rendere:
`select` ([tht-gate.js:492](../../../harness/.pi/extensions/tht-gate.js)), `multiselect` (576),
`artifact-gate` (639). La colonna **no-limbo** dice cosa sintetizzare quando la primitiva
nativa ritorna `undefined` (Esc / cancel / timeout / `signal`):
| widget | render interattivo | no-limbo (nativo → `undefined`) | UiResponse sintetizzato |
|---|---|---|---|
| `select` | `ctx.ui.select(title, [...labels, "Go back", "Exit", "Other — specify"])`; "Other"→`ctx.ui.input` | re-present una volta; se ancora `undefined` → `{control:"exit"}` | `{id, choices:[optId]}` o `{control:"back"/"exit"/"freetext", text}` |
| `multiselect` | `ctx.ui.custom` (checkbox list); fallback low-cost: `input` con indici separati da virgola | re-present una volta; poi `{control:"exit"}` | `{id, choices:[TUTTI gli id scelti]}` — vedi nota reviewer_decide sotto |
| `artifact-gate` | artefatto via `ctx.ui.editor(title, testo)` (multi-linea, meglio di `notify`) + `ctx.ui.confirm`/`select` approve/reject/other | re-present (advance solo su approve/reject **esplicito**, mai implicito) | `{id, choices:["approve"/"reject"]}` o control |
| `freetext` | `ctx.ui.input(title)` | re-present una volta; poi `{control:"exit"}` | `{id, text}` |
| `info` | già `ctx.ui.notify` — **verificato usato** (builders.js:127 → [tht-gate.js:569](../../../harness/.pi/extensions/tht-gate.js), avviso F2 memoria vuota) | n/a (fire-and-forget, nessun `id`) | — |
**Non sono widget-descriptor** (nessuna riga `present()` dedicata):
- **`freetext`** non è un widget a sé: è il `control:"freetext"` restituito dall'opzione
«Altro/Other» di `select`/`artifact-gate` → nel ramo TUI si raccoglie con `ctx.ui.input`
dentro il render di quei due.
- **`info`** non passa da `present()`: sono `ctx.ui.notify(..., "info")` scritti a mano
(es. [tht-gate.js:567](../../../harness/.pi/extensions/tht-gate.js), avviso F2 memoria vuota),
già mode-agnostic. `buildInfoRequest` **esiste** in `builders.js` ma **non è cablato** nel
gate (l'import a tht-gate.js:29-31 porta solo select/multiselect/artifact-gate) — builder morto.
## Vincoli del runtime Pi verificati (0.80.3)
@@ -137,6 +150,23 @@ gli si dà un renderer, invece di derivare silenziosamente dal percorso RPC.
L1-testabile in isolamento come `builders.js`.
- Nessun cambiamento a `builders.js`, ai classificatori, al backend o al frontend.
## Note implementative (checklist prima di scrivere present())
- **Il commento a [tht-gate.js:227](../../../harness/.pi/extensions/tht-gate.js)** dice
*"usa l'API UI NATIVA di Pi (ctx.ui.input)"*. Quando `emitAndWait` viene incapsulato in
`present()` con un ramo che usa i **widget TUI nativi** (non `ctx.ui.input`), quel commento
diventa fuorviante → va riscritto insieme al codice.
- **`ctx.ui.notify` da razionalizzare.** 6 notify nel gate; solo 319 e 397 sono guardate da
`ctx.hasUI`, le altre 4 sono **non guardate** (567, 651, 824, 831). Oggi in RPC sono
fire-and-forget verso il frontend; in TUI diventerebbero warning a terminale. Decidere
per-notify se appartiene a entrambe le modalità — in particolare la **651** è dentro il loop
di re-present del confirm gate.
- **Guardie `ctx.hasUI` da rivedere (regresso di migrazione).** 319/397 erano "solo TUI" con
la semantica 0.73.1 (`hasUI=false` in RPC). Su 0.80.3 `hasUI=true` anche in RPC, quindi ora
scattano in RPC (es. riga 321 *"Esc non chiude il gate…"*, priva di senso nel browser).
L'intento va espresso con `ctx.mode === "tui"`. **Indipendente dal dual-mode**, ma da
sistemare comunque.
## Test (il vero guardrail contro la deriva)
- **Property test round-trip** (non solo "descriptor→UiResponse"): per **ogni**
@@ -172,6 +202,7 @@ gli si dà un renderer, invece di derivare silenziosamente dal percorso RPC.
Fattibile e pulito: la doppia gestione si ottiene con **una sola cucitura mode-aware**
(`present`) attorno a `emitAndWait`, sfruttando che Pi è già mode-aware via `ctx.mode`
(`=== "tui"` per il ramo interattivo) e che a valle tutto consuma un `UiResponse`
interno. Il costo dipende dal livello di fedeltà interattiva voluto (C minimo, A
curato); il guardrail è il property test round-trip. Nessun impatto su RPC/frontend.
(`=== "tui"` per il ramo interattivo; **non** `hasUI`, che su 0.80.3 è `true` anche in RPC)
e che a valle tutto consuma un `UiResponse` interno. Il costo dipende dal livello di fedeltà
interattiva voluto (C minimo, A curato); il guardrail è il property test round-trip. Nessun
impatto su RPC/frontend.