diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index a46313fe..ea376129 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -255,6 +255,16 @@ Full session management modeled on Claude's UI, all three layers: 5. **Resolved: settings use `zai/glm-5.2`/medium** (not deepseek-flash); GLM 5.2 drives F1 fine, just slowly (~3-4 min, ~50+ reads). To tell a truly stalled Pi from a merely-slow one, check its sockets/children. Memory: `thothii-glm52-f1-slow-not-stuck.md`. +6. **DONE — Pi migrated to `@earendil-works/pi-coding-agent@0.80.3` (2026-07-04).** The old + scope `@mariozechner/pi-coding-agent` is frozen at 0.73.1; every release ≥0.74 lives under + the new scope `@earendil-works` (latest 0.80.3). The live `pi` (`~/.local/bin/pi`) was + repointed to 0.80.3. Validated by an API/RPC-surface diff (0.73.1→0.80.3: `ExtensionUIContext` + byte-identical, `rpc-types` additive-only, `createDialogPromise` + provider-registration API + unchanged) **plus** a live `model-matrix` smoke (GLM 5.2, `new`+`resume` both `CHAINED`, + full RPC event vocabulary incl. `extension_ui_request` intact). Notable: 0.80.3 adds + `ctx.mode: "tui"|"rpc"|"json"|"print"` (a proper mode discriminator; the earlier `0.79.4` + references above are superseded). Rollback: the old package is still on disk — repoint the + symlink to `@mariozechner/.../dist/cli.js`. Memory: `thothii-pi-earendil-migration.md`. ## Where design history lives - Specs: `docs/superpowers/specs/` · Plans: `docs/superpowers/plans/` diff --git a/docs/superpowers/specs/2026-07-04-dual-mode-gate-evaluation.md b/docs/superpowers/specs/2026-07-04-dual-mode-gate-evaluation.md new file mode 100644 index 00000000..3411bf38 --- /dev/null +++ b/docs/superpowers/specs/2026-07-04-dual-mode-gate-evaluation.md @@ -0,0 +1,177 @@ +# Valutazione: doppia gestione del gate (TUI nativo interattivo + widget-descriptor RPC) + +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. + +## Context + +Domanda: è pensabile una **doppia gestione** del gate HITL? Interattiva con le +primitive UI standard di Pi quando `pi` è lanciato da CLI, e quella attuale +(widget-descriptor JSON verso il frontend) quando è lanciato in RPC. + +Oggi il gate ha **una sola** via di emissione: `emitAndWait` → +`ctx.ui.input(JSON.stringify(descriptor), "")` ([tht-gate.js:300](../../../harness/.pi/extensions/tht-gate.js)). +In RPC il backend fa da bridge (`extension_ui_request` method `input` → SSE) e il +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)). + +## Verdetto: SÌ, è pensabile ed è architetturalmente supportato + +Pi **è già mode-aware**, quindi la doppia gestione non va "inventata", va solo +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. +- **`ExtensionUIContext`** (types.d.ts:63-67) — "Each mode (interactive, RPC, print) + provides its own implementation". Espone nativamente: + `select(title, options: string[], opts?) → Promise` (69), + `confirm(title, message, opts?) → Promise` (71), + `input(title, placeholder?, opts?) → Promise` (73), + `notify(message, type?)` (75), `editor(title, prefill?) → Promise` + (134), e `custom(factory) → Promise` (116, componente TUI a pieno controllo). + `info`/`notify` funzionano già in entrambe le modalità. + +## `UiResponse` è un tipo INTERNO ThothII (non un export di Pi) + +Tutta la logica a valle consuma un `UiResponse`: **non** è un tipo di Pi, è il +contratto condiviso tra il gate e il frontend/renderer per la risposta a un +descriptor. Va documentato esplicitamente (JSDoc typedef nel modulo del gate): + +```js +/** + * @typedef {Object} UiResponse Risposta a un widget-descriptor. Tipo INTERNO + * ThothII (NON esportato da Pi): contratto condiviso gate ↔ frontend/renderer. + * @property {string} [id] deve combaciare con descriptor.id (invariante emitAndWait) + * @property {string[]} [choices] id opzione/i scelte (select: 1 elem; multiselect: N) + * @property {string} [choice] forma singola, alternativa a choices + * @property {string} [control] "back" | "exit" | "freetext" | "cancel" + * @property {string} [text] testo libero (freetext) o input "Altro — specificare" + */ +``` + +## Design proposto (una sola cucitura, riuso di tutto il resto) + +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: + +- **Invariati**: `gate/builders.js` (costruzione descrittori) e i classificatori di + esito. Sono il contratto condiviso tra le due modalità. +- **Unica modifica**: sostituire `emitAndWait(ctx, descriptor)` con un + `present(ctx, descriptor): Promise` **mode-aware**: + - `if (ctx.mode !== "tui")` → percorso ATTUALE (RPC/json/print): + `ctx.ui.input(JSON.stringify(descriptor))` + `JSON.parse` + id-match. Nessun + cambiamento nel frontend. + - `else` (`ctx.mode === "tui"`, interattivo) → renderizza il descrittore con le + 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: + +| 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`) | — | + +## Vincoli del runtime Pi verificati (0.80.3) + +- **`ctx.ui.select` ritorna la label** (string), non l'id — `options: string[]` → + `Promise` (types.d.ts:69). Il renderer deve rimappare + label→`option.id`. `custom` evita il problema ma costa di più. +- **Nessuna multiselect nativa**: `ExtensionUIContext` non la espone → `custom` + (checkbox, livello A) o fallback a input indicizzato (livello C). +- **Dismiss / no-limbo nativo**: `select`/`confirm`/`input`/`editor` ritornano + `undefined` su cancel/Esc; `ExtensionUIDialogOptions` espone `signal?: AbortSignal` + (types.d.ts:37) e `timeout?` per chiudere il dialogo programmaticamente. Il renderer + **deve** gestire l'`undefined` esplicitamente (colonna no-limbo). +- **`editor` multi-linea** disponibile (types.d.ts:134) — preferibile a `notify` per + il testo dell'artefatto nel TUI. +- (Non verificabile dai soli tipi, e comunque **irrilevante**: se `custom` sia + supportato in RPC — lo usiamo **solo** nel ramo interattivo.) + +## Contratto d'estensibilità (nuovo `widget` kind) + +Per mitigare a livello di codice il rischio "due percorsi da tenere in sync": il seam +interattivo ha un `default:` esplicito. Un `widget` privo di renderer TUI dedicato +**non** deve half-funzionare in silenzio → `ctx.ui.notify(" non supportato in +TUI", "warning")` + re-present/abort controllato. Così `builders.js` + classificatori +restano l'unica fonte condivisa, e un kind nuovo **fallisce forte** in TUI finché non +gli si dà un renderer, invece di derivare silenziosamente dal percorso RPC. + +## Due livelli di implementazione (scegliibili in seguito) + +- **A — widget nativi Pi** (`select/confirm/editor/custom`): esperienza TUI curata; il + pezzo più costoso è la **multiselect** (Pi non ha una multiselect nativa → serve + `ctx.ui.custom`). +- **C — render testuale + input numerato** (minimo): in interattivo stampo il + descrittore ben formattato (titolo/intro/opzioni numerate) e leggo un numero/lettera + via `ctx.ui.input`, poi mappo su `UiResponse`. Molto meno lavoro, sufficiente "a + fini di valutazione/dev". **Consigliato** dato lo scopo dichiarato. + +## Superficie di modifica (se/quando si implementa) + +- `harness/.pi/extensions/tht-gate.js`: introdurre `present(ctx, descriptor)` e + sostituire le 3 chiamate a `emitAndWait` (reviewer_select/decide/confirm) con + essa; il ramo RPC è l'attuale `emitAndWait`. +- Nuovo modulo puro es. `harness/.pi/extensions/gate/interactive-render.js` + (descriptor → prompt testuale/native + parse risposta → `UiResponse`), così è + L1-testabile in isolamento come `builders.js`. +- Nessun cambiamento a `builders.js`, ai classificatori, al backend o al frontend. + +## Test (il vero guardrail contro la deriva) + +- **Property test round-trip** (non solo "descriptor→UiResponse"): per **ogni** + descriptor prodotto da `builders.js`, il `UiResponse` sintetizzato da + `interactive-render`, passato ai classificatori esistenti (`resolveSelectOutcome` / + `resolveConfirmOutcome` / `selectedChoice`), deve produrre **lo stesso** `{kind, ...}` + del `UiResponse` che il frontend invierebbe per lo stesso descriptor. È **questo** il + guardrail contro la deriva tra i due percorsi, non un test di sola forma. +- **reviewer_decide (multiselect) — lettura inline**: `reviewer_decide` **non** passa + dai classificatori; legge `resp.choices` **direttamente** + ([tht-gate.js:594](../../../harness/.pi/extensions/tht-gate.js): + `opts.filter(o => (resp.choices ?? []).includes(o.id))`). Il test di non-regressione + deve quindi verificare che la sintesi interattiva del multiselect popoli **tutti** gli + id scelti (non solo il primo), altrimenti in TUI si perderebbero selezioni. +- **Invariante id-match**: la sintesi interattiva deve impostare `id = descriptor.id` + (altrimenti `emitAndWait` scarta la risposta, tht-gate.js:312). +- **RPC invariato**: backend bridge + 115 test frontend + 53 test harness restano da + rigirare per non-regressione; il percorso descriptor non cambia. + +## Trade-off e limiti (onesti) + +- **Fedeltà interattiva ridotta**: niente modal ricco, niente **erDiagram**/grafo + schema-linking, niente rendering strutturato dell'artefatto — in TUI l'artefatto è + testo (via `editor`). Va benissimo per debug/valutazione, **non** per la review + clinica completa. +- **Multiselect**: nessuna primitiva nativa → `ctx.ui.custom` (livello A) o fallback + a input indicizzato (livello C). +- **Testabilità**: il render nativo richiede il runtime TUI di Pi (difficile da + unit-testare); la parte pura descriptor→`UiResponse` è testabile in L1 (round-trip + sopra). Il percorso RPC resta invariato e già coperto. + +## Conclusione + +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.