docs(pi): migrate runtime to @earendil-works@0.80.3; update dual-mode gate eval
- live pi repointed 0.73.1 (@mariozechner, frozen) -> 0.80.3 (@earendil-works); validated via API/RPC surface diff (stable+additive) + model-matrix smoke (GLM 5.2 new+resume CHAINED) - PROJECT_STATE: item 6 migration note (supersedes the 0.79.4 references) - dual-mode gate eval: integrate review points (#3,#5,#7,#8,#9,#10) and flip #1 to ctx.mode === "tui" (0.80.3 exposes ctx.mode: tui|rpc|json|print) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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/`
|
||||
|
||||
@@ -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<string|undefined>` (69),
|
||||
`confirm(title, message, opts?) → Promise<boolean>` (71),
|
||||
`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à.
|
||||
|
||||
## `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<UiResponse>` **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<string|undefined>` (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("<kind> 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.
|
||||
Reference in New Issue
Block a user