specrails-desktop 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dist/assets/{ActivityFeedPage-BTYWMRwB.js → ActivityFeedPage-LKqd18-G.js} +1 -1
- package/client/dist/assets/{AgentsPage-BfOCeHHt.js → AgentsPage-Cb-b-6Ot.js} +1 -1
- package/client/dist/assets/AnalyticsPage-HVxQQ1wy.js +1 -0
- package/client/dist/assets/{BarChart-DlshJN3Z.js → BarChart-BOyHB0dw.js} +1 -1
- package/client/dist/assets/{CodePage-DJCjDG4I.js → CodePage-DnOnwKGB.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-CTqZ9mbB.js → DesktopAnalyticsPage-D2auU39x.js} +1 -1
- package/client/dist/assets/{DocsDialog-KiJOSRvX.js → DocsDialog-CTuDX3GK.js} +1 -1
- package/client/dist/assets/{DocsPage-B17CR54A.js → DocsPage-DRyMmu0Z.js} +1 -1
- package/client/dist/assets/{ExportDropdown-BAu6z3b6.js → ExportDropdown-DO-GGiMh.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-CCG64Q-6.js → IntegrationsPage-BhbO4jFT.js} +1 -1
- package/client/dist/assets/{JobDetailPage-BnGJSMiS.js → JobDetailPage-DJooEg1s.js} +1 -1
- package/client/dist/assets/{JobsPage-B-tn4CIf.js → JobsPage-BbaC-YOg.js} +1 -1
- package/client/dist/assets/{addspec-DeDOztDr.js → addspec-B-BKlvDj.js} +1 -1
- package/client/dist/assets/{addspec-v8j6A7CD.js → addspec-BErjOdNK.js} +1 -1
- package/client/dist/assets/{addspec-B1FTtI2a.js → addspec-CIGb34PS.js} +1 -1
- package/client/dist/assets/{addspec-GWm4ffKl.js → addspec-C_3NBarY.js} +1 -1
- package/client/dist/assets/{addspec-Dw-0Dg-4.js → addspec-DDvvnE6N.js} +1 -1
- package/client/dist/assets/{addspec-DpRgmfmx.js → addspec-RuL8Zd7w.js} +1 -1
- package/client/dist/assets/{addspec-rp496P_F.js → addspec-rmhOaH7N.js} +1 -1
- package/client/dist/assets/{addspec-BCT9vm_c.js → addspec-xjDbYZWL.js} +1 -1
- package/client/dist/assets/{dist-js-B16c3VyT.js → dist-js-CiIVMsx3.js} +1 -1
- package/client/dist/assets/{dist-js-P2FkJ6fA.js → dist-js-Xc2lRKp2.js} +1 -1
- package/client/dist/assets/{index-AfVF6BgE.js → index-DK214dak.js} +45 -45
- package/client/dist/assets/index-DgKfQFcf.css +2 -0
- package/client/dist/assets/{lib-rNNmltMb.js → lib-Bo5s6xpe.js} +1 -1
- package/client/dist/assets/{settings-D3LurcR5.js → settings-BI_cVCqN.js} +1 -1
- package/client/dist/assets/{settings-5tzo0Rn3.js → settings-BRaLLSVi.js} +1 -1
- package/client/dist/assets/{settings-BEWv3VEu.js → settings-BcqH0oea.js} +1 -1
- package/client/dist/assets/settings-C0-7Fpxg.js +1 -0
- package/client/dist/assets/{settings-BORg56um.js → settings-D6QMBlGQ.js} +1 -1
- package/client/dist/assets/{settings-DcqWIEM6.js → settings-GOBKOTGl.js} +1 -1
- package/client/dist/assets/{settings-BDAW3trC.js → settings-pT3MzfRu.js} +1 -1
- package/client/dist/assets/{settings-Dfz8QbZS.js → settings-u-16ISHt.js} +1 -1
- package/client/dist/assets/{setup-D3rNZA9A.js → setup-BIIkb-_K.js} +1 -1
- package/client/dist/assets/{setup-C1IA-9YS.js → setup-BeQxu9kD.js} +1 -1
- package/client/dist/assets/{setup-pjgmYHx6.js → setup-CPa6GnlI.js} +1 -1
- package/client/dist/assets/{setup-gzLG8T6F.js → setup-CZl4OEJx.js} +1 -1
- package/client/dist/assets/{setup-C0dzw8j4.js → setup-ChpodNfn.js} +1 -1
- package/client/dist/assets/{setup-WP6WOYQh.js → setup-D_fjJH6u.js} +1 -1
- package/client/dist/assets/{setup-UD2aanGs.js → setup-YzD8DX4O.js} +1 -1
- package/client/dist/assets/{setup-CpfjaNut.js → setup-fRpDozmq.js} +1 -1
- package/client/dist/assets/{useProjectCache-Cid_GxRM.js → useProjectCache-DVNypkmR.js} +1 -1
- package/client/dist/index.html +5 -5
- package/docs/adding-a-provider.md +107 -0
- package/docs/agy-cli-provider-study.md +179 -0
- package/docs/gemini-cli-provider-study.md +301 -0
- package/docs/gemini-core-support-evaluation.md +160 -0
- package/docs/gemini.md +106 -0
- package/docs/internals/api-reference.md +4 -7
- package/package.json +2 -2
- package/server/dist/chat-manager.js +1 -1
- package/server/dist/core-package.js +6 -1
- package/server/dist/desktop-router.js +27 -8
- package/server/dist/explore-cwd-manager.js +1 -1
- package/server/dist/mobile/index.js +5 -5
- package/server/dist/mobile/mobile-admin-router.js +28 -35
- package/server/dist/mobile/mobile-datachannel.js +228 -0
- package/server/dist/mobile/mobile-gateway.js +72 -98
- package/server/dist/mobile/mobile-router.js +4 -35
- package/server/dist/mobile/mobile-signal-reconnect.js +84 -0
- package/server/dist/mobile/mobile-types.js +5 -5
- package/server/dist/mobile/mobile-webrtc-peer.js +129 -0
- package/server/dist/mobile/mobile-webrtc.js +117 -0
- package/server/dist/pricing.js +13 -0
- package/server/dist/project-router-tickets.js +63 -18
- package/server/dist/providers/gemini-adapter.js +234 -0
- package/server/dist/providers/index.js +4 -1
- package/server/dist/setup-manager.js +13 -7
- package/server/dist/setup-prerequisites.js +4 -0
- package/server/dist/spec-models.js +17 -3
- package/server/dist/util/cli-prompt.js +17 -1
- package/client/dist/assets/AnalyticsPage-AbVXKh9v.js +0 -1
- package/client/dist/assets/index-NlH5BbXJ.css +0 -2
- package/client/dist/assets/settings-yMubjqYw.js +0 -1
- package/server/dist/mobile/mobile-mdns.js +0 -81
- package/server/dist/mobile/mobile-pairing.js +0 -179
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Estudio: ¿Integrar `agy` (Antigravity CLI) como tercer proveedor en lugar de Gemini CLI?
|
|
2
|
+
|
|
3
|
+
> Documento de planificación (plan B / watch-item). Generado 2026-06-17 mediante investigación multi-agente de fuentes primarias (issue tracker `google-antigravity/antigravity-cli`, CHANGELOG, Google Developers Blog, GitHub API). Complementa `docs/gemini-cli-provider-study.md` (el plan principal). Lo marcado `[no verificado]` se señala explícitamente.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Respuesta directa
|
|
8
|
+
|
|
9
|
+
> ## VEREDICTO: **NO — todavía no.** Integra `gemini-cli` (path `GEMINI_API_KEY`) como tercer proveedor ahora, y trata `agy` como **watch-item gated por issues**, no como objetivo actual.
|
|
10
|
+
|
|
11
|
+
La premisa ("`agy` es el sucesor oficial, no quiero construir sobre algo deprecado") es **correcta a medias y, para nuestro contrato concreto, lleva a la conclusión opuesta**:
|
|
12
|
+
|
|
13
|
+
1. **El sunset NO mata el path que nos interesa.** Verificado contra fuente primaria (Google Developers Blog, "Transitioning Gemini CLI to Antigravity CLI"). El 18-jun-2026 lo que deja de servirse son **las peticiones de los logins de suscripción consumer (AI Pro / Ultra) y el tier gratuito**. Literalmente: *"Gemini CLI and Gemini Code Assist IDE extensions will stop serving requests for Google AI Pro and Ultra, as well as those using it free of charge."* Pero el path que un servidor desatendido usaría — **`GEMINI_API_KEY` de pago / enterprise** — **sobrevive explícitamente**: *"Gemini CLI will remain accessible via paid Gemini and Gemini Enterprise Agent Platform API keys"* y *"We'll continue to support Gemini CLI ... with access to the latest Gemini models and other updates."* El repo `google-gemini/gemini-cli` está **vivo** (`archived:false`, último push 2026-06-17, releases activas v0.46.0 estable / v0.48.0-nightly).
|
|
14
|
+
|
|
15
|
+
2. **`agy` no satisface el contrato hoy, en ninguna de sus 5 patas críticas a la vez.** Verificado contra el issue tracker (todos OPEN, cero respuesta de Google, a 2026-06-17): `#76` (stdout 0 bytes en non-TTY → rompe la captura básica de subproceso), `#7` (el conversation/session id **nunca** se expone → no hay handle que capturar para `--resume`), `#31` (sin `--acp`/JSON-RPC), `#119`/`#394` (sin `--output-format json`/stream-json), `#78` (OAuth-only, sin auth por API-key headless). Además `#85` (cerrado sin fix shipado) muestra que en **macOS** — nuestro target de escritorio — un proceso en background re-pide login por el timeout de 1s del keyring. **Coste/tokens no se exponen en NINGÚN sitio** (ni stdout, ni transcript.jsonl, ni CLI): la pata `cost` del contrato es directamente irresoluble hoy.
|
|
16
|
+
|
|
17
|
+
3. **El factor decisivo de bajo coste:** `agy` y `gemini-cli` **comparten el harness `~/.gemini`** (motor de agente común, settings/skills/MCP compartidos). Integrar `gemini-cli` ahora **no es tirar trabajo** si después migramos a `agy`: la auth, la config MCP y los modelos Gemini 3 ya nos llegan por `gemini-cli` apuntando al mismo `~/.gemini`. El swap a `agy` sería un cambio de transporte, no un re-platforming.
|
|
18
|
+
|
|
19
|
+
**En una frase:** estudiar `agy` *en lugar de* `gemini-cli` es prematuro porque (a) el binario de `gemini-cli` con API-key no muere, (b) `agy` headless está roto en las 5 patas con silencio total de Google, y (c) ambos comparten harness, así que `gemini-cli` ahora es el puente correcto hacia `agy` después.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Superficie del comando `agy`
|
|
24
|
+
|
|
25
|
+
`agy` es un agente de terminal en Go (bubbletea/TUI), **closed-source** (el repo solo aloja README + CHANGELOG + binarios + issue tracker). Versión actual **1.0.9** (2026-06-17), cadencia ~3 días. **El volcado verbatim de `agy --help` no se obtuvo** (docs JS-rendered); la superficie se reconstruyó del tracker (decisivo, cita flags contra versiones concretas), los CHANGELOG y specs operativos.
|
|
26
|
+
|
|
27
|
+
### Headless / scriptable
|
|
28
|
+
| Comando/flag | Notas |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `agy -p "<prompt>"` / `--print` | One-shot headless. **BUG `#76`/`#318`: en non-TTY emite 0 bytes a stdout y stderr, exit 0**, o cuelga. |
|
|
31
|
+
| `--prompt` | Alias/compañero de `-p` (issue `#7`). |
|
|
32
|
+
| `-i` / `--prompt-interactive` | One-shot y **se queda en TUI** → no headless puro. |
|
|
33
|
+
| `-c` / `--continue` | Resume la conversación **más reciente GLOBALMENTE** (peligroso para concurrencia). |
|
|
34
|
+
| `--conversation <id>` | Resume una conversación **específica** por id. Funciona con `-p` (issue `#278`). |
|
|
35
|
+
| `--model <name>` | Nombres descriptivos: `"Gemini 3.5 Flash (Low)"`, `"Gemini 3.1 Pro (High)"`. Default: Gemini 3.5 Flash. (1.0.5.) |
|
|
36
|
+
| `--sandbox` | Aislamiento. Propagación a `-p` arreglada en 1.0.6. |
|
|
37
|
+
| `--print-timeout <dur>` | p.ej. `60s`, `90s`. |
|
|
38
|
+
| `--log-file <path>` | **Trazas operativas (ids, request traces), NO payload de respuesta.** |
|
|
39
|
+
| `--add-dir <path>` | Añade directorio de workspace. |
|
|
40
|
+
| `--dangerously-skip-permissions` | Auto-aprueba todo ("YOLO"). |
|
|
41
|
+
| `--version`, `--help` / `help` | |
|
|
42
|
+
| `agy models` | Lista modelos (1.0.5). |
|
|
43
|
+
| `agy update`, `agy changelog` | Self-update / changelog. |
|
|
44
|
+
| `agy plugin <list\|import\|install\|uninstall\|enable\|disable\|validate\|link\|help>` | `agy plugin import gemini` importa config de gemini-cli. |
|
|
45
|
+
|
|
46
|
+
### Ausente (feature-requested, NO implementado en 1.0.9 — verbatim del tracker)
|
|
47
|
+
- **`--acp` / `agy acp`** (stdio JSON-RPC) → issue `#31` (OPEN, 81 comentarios, el más pedido), `#195`.
|
|
48
|
+
- **`--output-format json` / `stream-json` / NDJSON** → `#119`, `#394`. *`--output-format` es rechazado: `flags provided but not defined: -output-format`.* `-p` emite **un bloque de texto plano con `<thinking>` mezclado**.
|
|
49
|
+
- **`--session-id` / captura del conversation-id desde `-p`** → `#7`.
|
|
50
|
+
- **`--no-mcp` / `--mcp-config`** → `#342`.
|
|
51
|
+
- **`agy serve` / `agy api` / `agy run` / `agy agent`** → no existen. No hay daemon/HTTP local.
|
|
52
|
+
|
|
53
|
+
### TUI-only (solo dentro de una sesión activa)
|
|
54
|
+
Bare `agy`, `-i`, `-c`, y **todos** los slash commands: `/help /config /settings /model /mcp /skills /plugins /permissions /hooks /resume /tasks /diff /add-dir /open /btw /changelog /logout /export /goal /schedule /agent /context /usage /artifact` + modo shell `!`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Transportes de integración — tabla rankeada
|
|
59
|
+
|
|
60
|
+
Contra el contrato: **output machine-readable + captura session-id + resume + coste/tokens**.
|
|
61
|
+
|
|
62
|
+
| # | Transporte | Output M-R | Captura session | Resume | Coste/tokens | Viable hoy | Coste en el adapter |
|
|
63
|
+
|---|---|:---:|:---:|:---:|:---:|:---:|---|
|
|
64
|
+
| **2** | **transcript.jsonl on-disk** (+ watcher `brain/`) | ⚠️ texto parseado | ✅ (vía `last_conversations.json` o newest `brain/` dir) | ✅ (`--conversation <id>`) | ❌ no en disco | **⚠️ "el menos malo"** | Reader de JSONL undocumented + watcher de FS + plan de migración a SQLite |
|
|
65
|
+
| **1** | **pseudo-TTY** (`script`/`unbuffer`/node-pty) | ⚠️ texto TUI renderizado | ❌ | ❌ | ❌ | ⚠️ (solo como **complemento** de #2) | node-pty + strip de `<thinking>` + parsing de marcadores sembrados en el prompt |
|
|
66
|
+
| **4** | **SDK Python `google-antigravity`** | ✅ eventos tipados | ✅ in-process (no por id persistido) | ⚠️ in-process (no resume del CLI local) | ⚠️ `total_usage` (alpha, no documentado) | ⚠️ **re-platforming** | **Sidecar Python** separado; abandona el patrón binary-spawn; alpha 0.1.3 |
|
|
67
|
+
| **3** | **Managed Agents API / `agy serve`** | — | — | — | — | ❌ | No existe `agy serve`. La "Managed Agents API" es un **producto cloud separado** (Gemini API), no envuelve el `agy` local |
|
|
68
|
+
| **6** | **`agy -p` plano** | ❌ (`#76` 0 bytes) | ❌ (`#7`) | ⚠️ (id incapturable) | ❌ | ❌ | Roto on-arrival |
|
|
69
|
+
| **5** | **ACP / JSON-RPC stdio** (`--acp`) | ✅ (si existiera) | ✅ | ✅ | ⚠️ | ❌ **vaporware** | El transporte **más limpio** si Google lo shipea; hoy `#31` OPEN sin acuse |
|
|
70
|
+
|
|
71
|
+
### Lectura de la tabla
|
|
72
|
+
- **El "menos malo" viable hoy = #2 (transcript on-disk) + #1 (PTY) combinados.** PTY garantiza que el proceso completa y da texto de fallback; el transcript da output estructurado-ish (línea final `source=MODEL, status=DONE, type=PLANNER_RESPONSE`), captura de session (watch de `brain/` o `cache/last_conversations.json`) y resume (`agy --conversation <id> -p`). **Satisface 4 de 5 patas.**
|
|
73
|
+
- **La pata `cost` es el muro:** **ningún** source encontró `usageMetadata`/`tokenCount`/`input_tokens`/`cost` en transcript.jsonl, stdout, PTY ni CLI. Un `pricing.ts` rate-card como el de codex **no tiene de qué multiplicar** porque ni siquiera hay token counts fiables. Esta pata queda **sin resolver**.
|
|
74
|
+
- **Riesgo de durabilidad:** el schema JSONL es undocumented y está siendo **superado por SQLite** (`conversations/<id>.db`, 1.0.4+, dual-write 1.0.8+). Cualquier reader tiene fecha de caducidad.
|
|
75
|
+
- **El más limpio (#5 ACP) no existe.** El más completo en captura (#4 SDK) **no envuelve el `agy` CLI** — usa su propio runtime cloud bundled.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 4. Si se integrara `agy` — cómo sería el adapter (y por qué es más feo que el `gemini-adapter`)
|
|
80
|
+
|
|
81
|
+
Declararía contra el contrato `ProviderAdapter` (`server/providers/types.ts`):
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
capabilities: {
|
|
85
|
+
persistentStdin: false,
|
|
86
|
+
nativeResume: 'partial' // solo vía --conversation con UUID minteado por nosotros
|
|
87
|
+
nativeStreamJson: false,
|
|
88
|
+
nativeCostUsd: false,
|
|
89
|
+
nativeOtelEnv: false,
|
|
90
|
+
systemPromptArg: false // personas van por AGENTS.md / SKILL.md, no por flag
|
|
91
|
+
}
|
|
92
|
+
instructionsFilename: 'AGENTS.md'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Transporte:** #2 (transcript reader) + #1 (PTY wrap) combinados. Concretamente el adapter tendría que:
|
|
96
|
+
|
|
97
|
+
1. **Mintar nosotros el UUID de conversación** y pasarlo en `--conversation <uuid>` en el **primer** run (porque `#7`: el id auto-asignado nunca se expone). Esto invierte el patrón normal "spawn → captura session_id → resume".
|
|
98
|
+
2. **Envolver en node-pty** cada spawn para sortear `#76` (stdout 0-bytes en non-TTY). En Windows no hay `script` y `Start-Process -RedirectStandardOutput` cuelga; node-pty es el equivalente portable pero **[no verificado]** para `agy`.
|
|
99
|
+
3. **Watcher del directorio `~/.gemini/antigravity-cli/brain/`** para detectar el nuevo `<conversation-id>/` y leer `.system_generated/logs/transcript.jsonl`, parseando líneas undocumented (`source`/`status`/`type`) — con un **reader abstraído** que prevea la migración a `conversations/<id>.db` (SQLite).
|
|
100
|
+
4. **`pricing.ts` sin inputs:** para `ai_invocations` (model, tokens, cost) **no tenemos token counts de ninguna fuente**, así que `total_cost_usd` quedaría siempre `—` o un estimado inventado — viola la política de métricas honestas del Job Detail.
|
|
101
|
+
5. **Serializar la concurrencia con un lock**, porque `agy` reescribe `cache/last_conversations.json` en **cada** invocación. Eso **rompe el requisito de "concurrencia de varios rails"**: tendríamos que dar a cada rail un `--conversation` UUID distinto **y** un `HOME`/dir aislado, lo cual es **[no probado]**.
|
|
102
|
+
6. **Sin historia de auth desatendida:** OAuth-only (`#78`), keyring que re-pide login en background en macOS (`#85`). No hay `SPECRAILS_…_API_KEY` que inyectar como con codex.
|
|
103
|
+
|
|
104
|
+
**Por qué es más feo que el `gemini-adapter`:** el `gemini-adapter` es un **spawn de binario limpio** (mismo patrón que claude/codex: argv → `parseStreamLine` → `finaliseInvocationResult`), con `GEMINI_API_KEY` para auth desatendida, stream-json nativo y resume por session-id real. El `agy` adapter requeriría **tres puentes nuevos fuera del patrón** (node-pty, watcher de FS, posible sidecar) + un lock global de concurrencia + un coste **fabricado** o ausente. Es estrictamente peor en las 5 patas.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 5. El modelo de agentes/skills de `agy` para el pipeline (esto sí es bueno)
|
|
109
|
+
|
|
110
|
+
**Reconocimiento honesto: el modelo de agentes de `agy` es bueno y bien diseñado** — solo que la *integración headless* es lo que falla, no la expresividad del pipeline.
|
|
111
|
+
|
|
112
|
+
Hallazgo decisivo (guía de migración oficial, Google Cloud Community): **los `agent.json` declarativos del viejo Gemini CLI están OBSOLETOS.** Verbatim: *"Under Antigravity, subagents are orchestrated dynamically, making this directory obsolete"* → `rm -rf .agents/agents/`. **No hay un fichero que pre-defina una persona-subagente persistente nombrada**; los subagentes son emergentes (el orquestador llama a un tool `DefineSubagent` en runtime). No podemos shipear architect/developer/reviewer como tres `agent.json` que `agy` cargue.
|
|
113
|
+
|
|
114
|
+
**Lo que SÍ carga** (el árbol que `specrails-core` tendría que emitir, análogo a `.codex/skills` / `.claude/agents`):
|
|
115
|
+
|
|
116
|
+
1. **`AGENTS.md`** en la raíz del workspace → personas architect/developer/reviewer como **secciones de prompt** (Goal/Traits/Constraints).
|
|
117
|
+
2. **`.agents/skills/sr-architect/SKILL.md`**, `sr-developer/`, `sr-reviewer/` → un SKILL.md por fase, con frontmatter YAML `name:` + `description:` (third-person trigger) y cuerpo Goal/Instructions/Constraints. Cargan por **match semántico** del `description`. **También** surgen como slash commands.
|
|
118
|
+
3. **`.agents/workflows/sr-implement.md`** (frontmatter `description:`) → el slash command orquestador que secuencia architect→developer→reviewer, invocado `/sr-implement "<issue>"`.
|
|
119
|
+
|
|
120
|
+
**Forma del modelo:** **plano / un nivel.** El orquestador gestiona los handoffs; *"Agents do not call subagents; the workflow orchestrator manages handoffs."* La secuencia estricta hay que **forzarla en las instrucciones del workflow.md + constraints del AGENTS.md**.
|
|
121
|
+
|
|
122
|
+
**Crux a nuestro favor:** ese árbol vive bajo el **harness compartido `~/.gemini/config/` + `.agents/`**, leído idénticamente por la GUI de Antigravity 2.0 y por `agy` headless → **`specrails-core` PUEDE emitir un árbol `agy`**. Eso es trabajo de `specrails-core`, no nuestro.
|
|
123
|
+
|
|
124
|
+
**Gaps `[no verificado]` antes de comprometerse:** (a) ¿`agy -p "/sr-implement '...'"` expande deterministamente el workflow headless? (b) ¿secuenciación determinista de fases bajo orquestación dinámica?
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 6. Timeline / madurez
|
|
129
|
+
|
|
130
|
+
**Issues bloqueantes (GitHub API, 2026-06-17, todas OPEN, `author_association=NONE` en todos los comentarios → cero respuesta de maintainer/Google, ningún PR de fix mergeado):**
|
|
131
|
+
|
|
132
|
+
| Issue | Qué bloquea | Estado | Comentarios |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `#76` | stdout 0-bytes en non-TTY (el bloqueante #1 de spawn) | OPEN, sin fix | 19 |
|
|
135
|
+
| `#7` | conversation-id nunca expuesto (sin captura para resume) | OPEN | 1 |
|
|
136
|
+
| `#31` | ACP / JSON-RPC stdio (el transporte limpio) | OPEN, más pedido | 81 |
|
|
137
|
+
| `#78` | auth API-key headless (sin historia desatendida) | OPEN | 2 |
|
|
138
|
+
|
|
139
|
+
**Cadencia:** release cada ~3 días (1.0.4 el 06-01 → 1.0.9 el 06-17), pero **exclusivamente** en pulido TUI / hardening de sandbox / clipboard / statusline. **El contrato headless NO ha avanzado nada** entre 1.0.0→1.0.9.
|
|
140
|
+
|
|
141
|
+
**Madurez global:** preview público (no GA), closed-source, 339 issues abiertos. El SDK Python es **alpha 0.1.3**.
|
|
142
|
+
|
|
143
|
+
**Lectura:** el contrato usable **no está cerca**. Dada la velocidad gastada íntegramente en pulido TUI y el **silencio total de Google** en las cuatro issues integrator, una ETA realista es **issue-gated, no date-gated**, del orden de **2–4+ meses** si es que llega. **No integrar ahora.**
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 7. Recomendación final + ruta
|
|
148
|
+
|
|
149
|
+
> **AHORA:** Integra **`gemini-cli` como tercer proveedor** vía el `gemini-adapter` ya diseñado (binary-spawn limpio, `GEMINI_API_KEY`, stream-json + resume nativos). **`agy` = watch-item gated.**
|
|
150
|
+
|
|
151
|
+
**Justificación:**
|
|
152
|
+
- El sunset mata **el login de suscripción consumer + free**, NO el binario open-source con API-key de pago/enterprise — que Google se compromete a seguir soportando *con los modelos nuevos*.
|
|
153
|
+
- **El modelo Gemini 3 ya nos llega vía `gemini-cli`** apuntando a `GEMINI_API_KEY`. No hay nada que ganar del lado del modelo cambiando a `agy` hoy.
|
|
154
|
+
- **Harness compartido `~/.gemini`:** integrar `gemini-cli` ahora **no es trabajo desechable** — la auth, MCP y el árbol de config quedan asentados en el mismo `~/.gemini` que `agy` reusaría. Un swap futuro sería cambio de transporte.
|
|
155
|
+
- `agy` headless está roto en **las 5 patas a la vez**, con la pata **coste irresoluble** y **sin auth desatendida** en macOS, con silencio de Google.
|
|
156
|
+
|
|
157
|
+
### Si en el futuro `gemini-cli` muriera de verdad para el path API-key
|
|
158
|
+
No lo hace (confirmado que sobrevive). Pero **si** llegara a morir, el puente provisional sería el **transporte #2 (transcript.jsonl) + #1 (PTY wrap)**, con coste marcado como estimado-imposible y concurrencia serializada con lock — explícitamente temporal y degradado.
|
|
159
|
+
|
|
160
|
+
### Condiciones (issues cerradas con fix verificado) que dispararían el cambio a `agy` — **gate AND**:
|
|
161
|
+
- ✅ **`#76` cerrada** (stdout en non-TTY capturable), **Y**
|
|
162
|
+
- ✅ **`#7` shipado** (conversation-id capturable desde `-p`), **Y**
|
|
163
|
+
- ✅ **`#31` (`--acp`/JSON-RPC)** *o* **`--output-format stream-json` (`#119`/`#394`)**, **Y**
|
|
164
|
+
- ✅ **`#78` shipado** (auth por API-key + fix del keyring `#85`), **Y**
|
|
165
|
+
- ✅ una fuente de **token usage / coste** machine-readable (hoy inexistente — el bloqueante silencioso más duro), **Y**
|
|
166
|
+
- ✅ concurrencia segura sin el lock global de `last_conversations.json`.
|
|
167
|
+
|
|
168
|
+
**Vía paralela (no la principal):** si se prioriza el modelo de agentes de `agy`, el trabajo correcto es que **`specrails-core` aprenda a emitir el árbol `agy`** (`AGENTS.md` + `.agents/skills/sr-*/SKILL.md` + `.agents/workflows/sr-implement.md`), exactamente como ya emite `.codex/skills` — independiente de nuestro adapter, se puede empezar antes de que el transporte madure.
|
|
169
|
+
|
|
170
|
+
**Watch concreto:** revisar `#76`, `#7`, `#31`/`#119`, `#78` (y el SDK Python alcanzando beta con `total_usage` estable) cada pocas semanas. Hasta que el gate AND se cumpla: **`gemini-cli` ahora, `agy` después.**
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### Notas de honestidad sobre lo incierto
|
|
175
|
+
- `agy --help` verbatim **no obtenido** — superficie reconstruida del tracker/CHANGELOG (alta confianza por estar versionada) + specs operativos.
|
|
176
|
+
- **Coste/tokens:** ninguna fuente encontró campos de usage en transcript.jsonl/stdout/CLI — la imposibilidad de la pata coste es por *ausencia de evidencia en cuatro investigaciones*, no por negativa explícita de Google; a efectos prácticos es bloqueante.
|
|
177
|
+
- **node-pty para `agy` en Windows:** equivalente portable a `script`, **no verificado** contra `agy`.
|
|
178
|
+
- **SDK Python:** resume de una conversación `agy` local por id **no documentado**; gestiona su propio estado con runtime cloud bundled — re-platforming, no wrapping.
|
|
179
|
+
- **`agy -p "/workflow"`** expandiendo deterministamente headless: **no verificado**.
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Estudio de implementación — Gemini CLI como tercer proveedor de specrails-desktop
|
|
2
|
+
|
|
3
|
+
> Documento de planificación (no contrato final). Generado 2026-06-16 mediante auditoría multi-agente del código real + investigación de fuentes primarias de `github.com/google-gemini/gemini-cli` (HEAD `5624a3b`, v0.48.0-nightly). Listón de referencia: el adapter de Codex (no-nativo con puentes). Las citas `file:line` provienen de la auditoría del código; las flags/campos de Gemini en `verbatim` de la investigación. Lo marcado `[UNCERTAIN]` se señala con su plan de resolución.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Resumen ejecutivo y alcance
|
|
8
|
+
|
|
9
|
+
**Veredicto: viable y de bajo riesgo arquitectónico.** El core de spawn/detect/cost/registry ya está generalizado por id de proveedor (`server/providers/registry.ts` es un `Map<ProviderId,adapter>`, `core-compat.ts` recorre `listAdapters()`, `pricing.ts` usa claves `'<id>:<model>'`, `result-event.ts` `finaliseInvocationResult` y `provider-selection.ts` son membership-based). El contrato promete *"un fichero + una entrada de registro"* y eso se cumple para el camino de compilación mínimo. El resto del trabajo es **ensanchar uniones literales `'claude'|'codex'`** y **rellenar branches `=== 'codex'` que no tienen hermano gemini**.
|
|
10
|
+
|
|
11
|
+
Gemini supera a Codex en dos capacidades clave verificadas:
|
|
12
|
+
- **OTEL nativo** (`[confirmed]`): emite OTLP por env (`GEMINI_TELEMETRY_*`), igual que claude → `nativeOtelEnv: true`, **no necesita bridge sintético**.
|
|
13
|
+
- **System-prompt nativo** (`[confirmed]`): `GEMINI_SYSTEM_MD=<path>` reemplaza el system prompt por env de proceso → soporte de system-prompt (con matiz, ver §3).
|
|
14
|
+
|
|
15
|
+
**Entra en v1 (PR-B beta-gated):** jobs/rails básicos, Explore Spec multi-turno (spawn-per-turn con `--resume`), Quick spec, Analytics/coste vía rate-card, OTEL nativo, terminal CLI launch, detección + prerequisitos, selección de modelo, multi-provider per project.
|
|
16
|
+
|
|
17
|
+
**NO entra en v1 (paridad Codex, se ocultan por intersección de capacidades):** Agent Profiles, SMASH, Contract Refine, Ultracode (+interactivo), plugins/Serena, persistent-stdin de Explore. Gemini se comporta byte-idéntico a Codex en estas superficies **siempre que declare las capacidades correctas y se ensanchen los tipos** — cero código nuevo en esas superficies.
|
|
18
|
+
|
|
19
|
+
**Diferido a follow-up explícito (decisiones tomadas, no implementadas en v1):** generalizar `provider-capabilities.ts` para *otorgar* a Gemini features hoy claude-only; bridge Windows multi-line argv; traducción slash-command para rails funcionales en Gemini (esto último es **bloqueante para rails reales**, ver §6).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Modelos a ofrecer
|
|
24
|
+
|
|
25
|
+
Decisión: **pinear ids GA concretos, no alias ni ids preview**, porque los ids `gemini-3-*-preview` rotan (la API ya movió Pro a `gemini-3.1-pro-preview`) y el CLI acepta ids inválidos sin validar (`issue #21391`). `ai_invocations.model` y la clave de pricing necesitan un id concreto y estable → el `modelCatalog()` ofrece ids GA concretos como `value`, que son los que se pasan a `--model` y se almacenan; se evita el churn de los preview.
|
|
26
|
+
|
|
27
|
+
**Catálogo curado (4 entradas):**
|
|
28
|
+
|
|
29
|
+
| value (`--model` / `ai_invocations.model`) | label | default |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `gemini-2.5-pro` | Gemini 2.5 Pro | **sí** |
|
|
32
|
+
| `gemini-2.5-flash` | Gemini 2.5 Flash | no |
|
|
33
|
+
| `gemini-2.5-flash-lite` | Gemini 2.5 Flash Lite | no |
|
|
34
|
+
| `gemini-3.1-pro-preview` | Gemini 3 Pro (preview) | no |
|
|
35
|
+
|
|
36
|
+
- **Default = `gemini-2.5-pro`**: GA, no-preview, es el `DEFAULT_GEMINI_MODEL` hardcoded del propio CLI, pricing estable. Evita anclar el producto a un id preview que rotará.
|
|
37
|
+
- **Se ofrece `gemini-3.1-pro-preview`** como cuarta opción de calidad (el único Pro de Gemini-3 *con precio publicado*; `gemini-3-pro` no existe como modelo con precio). Etiquetado "preview" para señalar volatilidad.
|
|
38
|
+
- **Se omiten `gemini-3-flash-preview` / `gemini-3.5-flash` / `gemini-3.1-flash-lite`**: preview de precio movible; con `flash`/`flash-lite` 2.5 ya cubrimos el tier rápido/barato con GA estable. Añadirlos luego es trivial (solo filas de pricing + catálogo).
|
|
39
|
+
- **Se omite pasar alias `auto`/`pro`/`flash`**: el routing `auto` impide stampear un id concreto en `ai_invocations.model`, rompiendo la atribución de coste. Pinneamos ids.
|
|
40
|
+
|
|
41
|
+
`[UNCERTAIN]`: el mapping alias→id-concreto no está byte-documentado; se resuelve NO usando alias.
|
|
42
|
+
|
|
43
|
+
**Filas para `server/pricing.ts` (clave `'gemini:<model>'`, USD por 1M tokens, Standard paid tier):**
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Gemini (Google). Ref: ai.google.dev/gemini-api/docs/pricing (fetched 2026-06-16).
|
|
47
|
+
// nativeCostUsd:false → estas tarifas son la ÚNICA fuente de coste.
|
|
48
|
+
'gemini:gemini-2.5-pro': { inputPer1M: 1.25, outputPer1M: 10.00, cacheReadPer1M: 0.125, lastReviewedAt: '2026-06-16' },
|
|
49
|
+
'gemini:gemini-2.5-flash': { inputPer1M: 0.30, outputPer1M: 2.50, cacheReadPer1M: 0.03, lastReviewedAt: '2026-06-16' },
|
|
50
|
+
'gemini:gemini-2.5-flash-lite': { inputPer1M: 0.10, outputPer1M: 0.40, cacheReadPer1M: 0.01, lastReviewedAt: '2026-06-16' },
|
|
51
|
+
'gemini:gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00, cacheReadPer1M: 0.20, lastReviewedAt: '2026-06-16' },
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Notas de tiering y caché (decisiones explícitas):
|
|
55
|
+
- **`gemini-2.5-pro` y `gemini-3.1-pro-preview` son context-tiered** (>200k tokens duplica precio: pro $2.50/$15.00; 3.1-pro $4.00/$18.00). `PriceEntry` no soporta tiering por tamaño de prompt. **Decisión v1: usar el tier ≤200k** (la mayoría de jobs caen ahí). El coste de prompts >200k se **infra-estima** — aceptable y honesto (Analytics ya marca `estimated=true`). Fidelidad = follow-up que ensancha `PriceEntry` con tiering (toca también codex pro). No bloqueante.
|
|
56
|
+
- **Free tier**: existe (OAuth 60 RPM/1000 RPD; API key free 10 RPM/250 RPD, solo Flash). **No se modela en pricing** — el rate-card es para coste estimado, no para gating de cuota (coherente con codex).
|
|
57
|
+
- **Caché**: `cacheReadPer1M` asume (como OpenAI/codex) que los cached tokens son un *subconjunto* de `tokens_in`. Gemini reporta `cachedContentTokenCount` separado → `[UNCERTAIN]` **resolución**: en `extractResult` mapear `cached` a `tokens_cache_read` y NO restarlo de `tokens_in` (mismo patrón que codex `cached_input_tokens`); el test de pricing fija el contrato.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. El adapter `server/providers/gemini-adapter.ts`
|
|
62
|
+
|
|
63
|
+
Estructura espejo de `codex-adapter.ts`. Constantes `GEMINI_MODELS`, `GEMINI_MIN_VERSION`, helper `fold()` (igual que codex-adapter.ts:49-52), `WHICH_CMD`, `compareSemver`.
|
|
64
|
+
|
|
65
|
+
### Propiedades readonly
|
|
66
|
+
|
|
67
|
+
| Miembro | Valor Gemini | Razón |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `id` | `'gemini'` | clave de registro; igual a `projects.providers[]` y al param `aiEngine`. |
|
|
70
|
+
| `displayName` | `'Gemini CLI'` | etiqueta UI pura. |
|
|
71
|
+
| `binary` | `'gemini'` | ejecutable en PATH. |
|
|
72
|
+
| `minCliVersion` | `'0.11.0'` | piso donde stream-json (PR #10883) + resume están validados. <0.6 no tiene `--output-format`; <0.11 no tiene stream-json. |
|
|
73
|
+
| `projectDirName` | `'.gemini'` | dir de settings del CLI (`.gemini/settings.json`). |
|
|
74
|
+
| `instructionsFilename` | `'GEMINI.md'` | el CLI lee GEMINI.md por defecto; plugin/explore-cwd escriben aquí. **Mejor que codex**: nativo, sin config extra. |
|
|
75
|
+
| `mcpRegistration` | `'project-json'` | Gemini registra MCP en `.gemini/settings.json` `mcpServers`. `gemini mcp add -s project` escribe al settings.json. |
|
|
76
|
+
|
|
77
|
+
### `capabilities` (8 campos)
|
|
78
|
+
|
|
79
|
+
| Campo | Valor | Por qué |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `nativeResume` | `true` | `--resume <uuid\|index\|latest>` headless wired en non-interactive (`nonInteractiveCli.ts:236-242`, `resumeChat`). El host pre-asigna UUID con `--session-id` y resume con `--resume <uuid>`. |
|
|
82
|
+
| `nativeStreamJson` | `true` | `--output-format stream-json` emite NDJSON (`init/message/tool_use/tool_result/error/result`). |
|
|
83
|
+
| `nativeCostUsd` | `false` | NO hay campo cost/usd en ningún evento (grep confirmado). → rate-card en pricing.ts (igual codex). |
|
|
84
|
+
| `nativeOtelEnv` | `true` | **MEJOR que codex**: emite OTLP nativo por `GEMINI_TELEMETRY_*` env. No necesita bridge sintético (ver §4). |
|
|
85
|
+
| `profileEnvSupport` | `true` (vestigial) | Igual que codex: rails fuerzan profile=null para non-claude (`rails-router.ts:250`); valor irrelevante funcionalmente; `true` por paridad. |
|
|
86
|
+
| `systemPromptArg` | `false` (v1) | **Matiz**: Gemini SÍ soporta override de system-prompt, pero por **env `GEMINI_SYSTEM_MD=<path>`**, NO por flag de argv. El contrato `systemPromptArg` significa "¿hay un *flag* `--system-prompt`?" → no lo hay. **v1: `false`** y foldear systemPrompt en el prompt (como codex, fold()). El path env es follow-up. |
|
|
87
|
+
| `persistentStdin` | omitido (`false`) | El CLI requiere el follow-up prompt vía `-p`/`--prompt` en resume headless (issue #14180), no soporta stdin stream-json long-lived documentado. → Explore cae al spawn-per-turn legacy (sin pérdida vs default, que es OFF). |
|
|
88
|
+
|
|
89
|
+
**Nota sobre `systemPromptArg=false` pese a soporte nativo**: el contrato solo modela "flag de argv". Como Gemini usa env (`GEMINI_SYSTEM_MD`), la vía limpia v1 es foldear (codex-parity). Un follow-up puede inyectar `GEMINI_SYSTEM_MD` en el env de spawn — pero eso es un reemplazo *completo* del system prompt (no merge), y los managers asumen append; foldear es más seguro en v1.
|
|
90
|
+
|
|
91
|
+
### `buildArgs(action, opts)` — switch de los 9 SpawnActions
|
|
92
|
+
|
|
93
|
+
Flags base: `--output-format stream-json`, `--model <opts.model>`. Headless se dispara con `-p <prompt>`. Jobs autónomos: `--approval-mode yolo`. Acciones read-only de spec/explore: `--approval-mode plan`. `opts.extraArgs` siempre se appendea verbatim.
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
chat-turn (Explore): gemini -p <fold(sys,prompt)> --model <m> --output-format stream-json --session-id <hostUUID>
|
|
97
|
+
chat-resume: gemini -p <fold(sys,prompt)> --model <m> --output-format stream-json --resume <opts.sessionId>
|
|
98
|
+
// throw si !opts.sessionId (igual codex)
|
|
99
|
+
chat-stream: throw // persistentStdin no soportado (igual codex)
|
|
100
|
+
rail-job: gemini -p <command> --model <m> --output-format stream-json --approval-mode yolo --session-id <hostUUID>
|
|
101
|
+
// NB: el command slash /specrails:X NO lo entiende Gemini → §6 (bloqueante rails reales)
|
|
102
|
+
spec-gen: gemini -p <fold(sys,prompt)> --model <m> --output-format stream-json --approval-mode plan
|
|
103
|
+
agent-refine: gemini -p <fold(sys,prompt)> --model <m> --output-format stream-json --approval-mode yolo
|
|
104
|
+
setup-enrich: gemini -p <fold(sys,prompt)> --model <m> --output-format stream-json
|
|
105
|
+
setup-enrich-resume: gemini -p <fold(sys,prompt)> --model <m> --output-format stream-json --resume <opts.sessionId>
|
|
106
|
+
// throw si !opts.sessionId
|
|
107
|
+
auto-title: gemini -p <fold(sys,prompt)> --model <m> --output-format stream-json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Detalles:
|
|
111
|
+
- **`--session-id <hostUUID>` en turno 1** (chat-turn, rail-job): el host mintea un UUID propio y lo pre-asigna, así no depende de parsear `init.session_id` (que es post-2025-12-04). Resume luego con `--resume <eseUUID>`. Robustez de versión.
|
|
112
|
+
- **Resume requiere prompt vía `-p`** (no stdin/positional) — issue #14180, workaround estable documentado.
|
|
113
|
+
- **`opts.maxTurns`**: Gemini lo mapea a `maxSessionTurns` (settings.json, default 100), NO hay flag `--max-turns`. Ignorar `maxTurns` en buildArgs (como codex); el cap sale por exit code 53.
|
|
114
|
+
- **cwd**: todos los spawns desde `project.path` (o explore-cwd) — el resume está scopeado por hash de cwd (`getProjectHash(process.cwd())`). El host ya fija `cwd=project.path` por manager.
|
|
115
|
+
|
|
116
|
+
### `parseStreamLine(line)` — mapeo evento-gemini → AdapterEvent[]
|
|
117
|
+
|
|
118
|
+
`JSON.parse` por línea; `null` en línea vacía o parse-fail (igual codex):
|
|
119
|
+
|
|
120
|
+
| Evento Gemini | AdapterEvent | Mapeo |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `init` `{session_id,model}` | `session-started{sessionId: e.session_id}` | **session_id sale del evento `init`** (único sitio). |
|
|
123
|
+
| `message` `{role:'assistant',content,delta}` | `text-delta{text: e.content}` | acumular chunks con `role==='assistant'`. `role:'user'` (echo) → ignorar. |
|
|
124
|
+
| `tool_use` `{tool_name,tool_id,parameters}` | `tool-use{name: e.tool_name, inputPreview: ...slice(0,200)}` | preview 200 chars (igual codex). |
|
|
125
|
+
| `tool_result` `{tool_id,status,output,error}` | `other{type:'tool_result',raw}` | no hay variant dedicado; codex tampoco lo usa. |
|
|
126
|
+
| `result` `{status,stats}` | `result{payload: e}` | terminal; `stats` queda en payload para `extractResult`. |
|
|
127
|
+
| `error` `{severity,message}` | `other{type:'error',raw}` | no-fatal; los fatales van a stderr+exit. |
|
|
128
|
+
| desconocido | `other{type,raw}` | |
|
|
129
|
+
|
|
130
|
+
**session_id**: del evento `init.session_id`. Como además pre-asignamos `--session-id`, el host ya lo conoce sin parsear (doble seguridad).
|
|
131
|
+
|
|
132
|
+
### `extractResult(events)` — suma de tokens
|
|
133
|
+
|
|
134
|
+
Lee el último `result.payload.stats` (StreamStats, flat):
|
|
135
|
+
- `tokens_in ← stats.input_tokens`
|
|
136
|
+
- `tokens_out ← stats.output_tokens` (Gemini stream-json **descarta** `thoughts`/`tool` tokens; no hay reasoning separado que foldear)
|
|
137
|
+
- `tokens_cache_read ← stats.cached`
|
|
138
|
+
- `tokens_cache_create ← undefined`
|
|
139
|
+
- `num_turns ←` contar eventos `result` (1 por turno)
|
|
140
|
+
- `session_id ←` último `session-started.sessionId`
|
|
141
|
+
- `total_cost_usd ← undefined` → `finaliseInvocationResult` (`result-event.ts:48-89`) aplica `estimateCostUsd('gemini', model, usage)` contra `pricing.ts`. Sin filas → coste NULL + warn (`result-event.ts:81`).
|
|
142
|
+
|
|
143
|
+
### `detectInstalled()` — espejo exacto de codex (contrato cap 3s, types.ts:131-132)
|
|
144
|
+
|
|
145
|
+
`which/where gemini` → si ausente `{installed:false,executable:false}`. Si presente: `gemini --version` con `timeout:3000`, regex `\d+\.\d+\.\d+`, `compareSemver` vs `GEMINI_MIN_VERSION='0.11.0'` → `meetsMinimum` + string de error/upgrade. Fallo del probe → `{installed:true,executable:false}`. Corre al startup y en `/setup-prerequisites`.
|
|
146
|
+
|
|
147
|
+
### `baselineAgents()` → `['sr-architect','sr-developer','sr-reviewer']` (vestigial — rails fuerzan profile=null).
|
|
148
|
+
|
|
149
|
+
### `registration` (`server/providers/index.ts:11-14,17`)
|
|
150
|
+
```ts
|
|
151
|
+
import { geminiAdapter } from './gemini-adapter'
|
|
152
|
+
register(geminiAdapter)
|
|
153
|
+
export { claudeAdapter, codexAdapter, geminiAdapter }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 4. OTEL: nativo (NO bridge)
|
|
159
|
+
|
|
160
|
+
**Decisión: `nativeOtelEnv: true` — Gemini va por el path de claude (env-injection), NO por el bridge sintético de codex.** Evidencia `[confirmed]`: `packages/core/src/telemetry/config.ts` (PR #9113) lee de `process.env` con precedencia env>settings, emitiendo OTLP nativo de traces+metrics+logs.
|
|
161
|
+
|
|
162
|
+
**Cómo se inyecta** — en `server/queue-manager.ts`, con `nativeOtelEnv:true` el bloque del bridge (`queue-manager.ts:1197-1205`, gated `!adapter.capabilities.nativeOtelEnv`) **se salta automáticamente**, y Gemini fluye por `buildTelemetryEnv` (`queue-manager.ts:1042,1105`). El adapter/queue inyecta:
|
|
163
|
+
```
|
|
164
|
+
GEMINI_TELEMETRY_ENABLED=true
|
|
165
|
+
GEMINI_TELEMETRY_TARGET=local
|
|
166
|
+
GEMINI_TELEMETRY_OTLP_ENDPOINT=<app OTLP receiver>
|
|
167
|
+
GEMINI_TELEMETRY_OTLP_PROTOCOL=grpc # grpc:4317, evita el bug OTLP/HTTP #15581
|
|
168
|
+
GEMINI_TELEMETRY_TRACES_ENABLED=true # spans (off por defecto)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Qué cambia respecto a claude en queue-manager**: `buildTelemetryEnv` hardcodea `CLAUDE_CODE_ENABLE_TELEMETRY:'1'` (`queue-manager.ts:52-60`), claude-específico. Gemini necesita SU enable-flag. **Cambio requerido (solo si telemetry está ON para un job gemini)**: parametrizar el enable-var por adapter — p.ej. `adapter.nativeOtelEnvVars()` que devuelva `{CLAUDE_CODE_ENABLE_TELEMETRY:'1'}` para claude y el bloque `GEMINI_TELEMETRY_*` para gemini. Los `OTEL_EXPORTER_OTLP_*` estándar siguen neutrales. **No bloqueante** salvo que se active telemetry.
|
|
172
|
+
|
|
173
|
+
**Caveat de correlación `[UNCERTAIN]`**: el receptor OTLP del app rutea por `specrails.job_id` + `specrails.project_id` en `resource.attributes`. Gemini emite sus propios `session.id`/`installation.id`, NO los de specrails. Si Gemini honra `OTEL_RESOURCE_ATTRIBUTES` (estándar OTEL SDK, **no documentado en telemetry.md**), inyectar `OTEL_RESOURCE_ATTRIBUTES=specrails.job_id=<id>,specrails.project_id=<id>` resuelve la correlación. **Resolución**: probar empíricamente antes de confiar; fallback = correlacionar por `session.id` de spawn-time (lo conocemos vía `--session-id`). Telemetry es opt-in OFF por defecto → no bloquea v1.
|
|
174
|
+
|
|
175
|
+
**El bridge `codex-otel-bridge.ts` NO se toca** para Gemini (se bypassa por capability).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 5. Change-list exacto por fichero (dos PRs)
|
|
180
|
+
|
|
181
|
+
### PR-A — Generalización previa (ensanchar tipos, desramificar; sin Gemini todavía)
|
|
182
|
+
|
|
183
|
+
Mergeable independiente, no cambia comportamiento observable.
|
|
184
|
+
|
|
185
|
+
| Fichero:línea | Cambio | Bloq |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `server/desktop-db.ts:10,50,167,268,388` | `CliProvider = 'claude'\|'codex'` → ensanchar a incluir `'gemini'` (o alias a `ProviderId`/string). Tipo DB central; todos heredan. `DEFAULT 'claude'` se mantiene. | **sí** |
|
|
188
|
+
| `server/spec-models.ts:1-37` | `SpecProvider` union + reemplazar ternario `provider==='codex'?CODEX:CLAUDE` en `getModelsForProvider` por **lookup `Record<provider, SpecModelOption[]>`** (fallback `adapter.defaultModel()`/`[]`). `PROVIDER_DEFAULT_MODEL` → record extensible. | **sí** |
|
|
189
|
+
| `server/desktop-router.ts:158-168` (`GET /available-providers`) | Dejar de destructurar `{claude,codex}` fijo; `detectAvailableCLIs()` ya devuelve `Record<string,boolean>` → **devolver el map entero (spread)**. `if(providers.claude\|\|providers.codex)` → `Object.values(providers).some(Boolean)`. | **sí** |
|
|
190
|
+
| `server/desktop-router.ts:265-266` | casts `as 'claude'\|'codex'` en addProject → ensanchar (registry valida con `hasAdapter`). | **sí** |
|
|
191
|
+
| `server/project-registry.ts:116` | `AddProjectInput.providers?: ('claude'\|'codex')[]` → `ProviderId[]`/`string[]`. | **sí** |
|
|
192
|
+
| `client/src/hooks/useDesktop.tsx:29,35,48,172` | 4 anotaciones `('claude'\|'codex')[]` → `string[]`/`ProviderId`. | **sí** |
|
|
193
|
+
| `client/src/lib/provider-capabilities.ts:11` | `ProviderId = 'claude'\|'codex'` → añadir `'gemini'`/string. | **sí** |
|
|
194
|
+
| `client/src/components/ModelSelector.tsx:20-55,80` | ternario `provider==='claude'?CLAUDE:CODEX` → map por id; `PRESET_DEFAULTS`/`MAX_OVERRIDES` literal → `Record<string,string>`; ensanchar prop union. **Bloqueante funcional**: sin esto Gemini mostraría modelos de Codex. | **sí** |
|
|
195
|
+
| `client/src/components/ChatInput.tsx:7-18,25` | selección por id-keyed map; ensanchar `provider?: 'claude'\|'codex'`. | **sí** |
|
|
196
|
+
| `client/src/components/AddProjectDialog.tsx:28,31,37,41,67-79,96,141,235-236` | `availableProviders` de `{claude;codex}` fijo → `Record<string,boolean>` iterado sobre `/available-providers`; `!claude && !codex` → iterar keys. | **sí** |
|
|
197
|
+
| `client/src/components/{AiEngineSelector:40,53, RailEngineSelector:33,38, CliLaunchMenu:52,55, explore-spec/SpecModelPicker:23,81, SetupWizard:563}` | casts inline `as 'claude'\|'codex'` → ensanchar union. Listas data-driven, Gemini aparece solo. | **sí** |
|
|
198
|
+
| `server/util/cli-prompt.ts:178-191` | `spawnAiCli` ya cae a `spawnCli` genérico para binarios desconocidos → **POSIX funciona sin cambio**. Windows multi-line argv (`transformClaudeArgsForWindows`/`transformCodexArgsForWindows`) NO cubre gemini. | cosmético (POSIX) / bloq Windows |
|
|
199
|
+
|
|
200
|
+
### PR-B — El adapter Gemini + branches y catálogos
|
|
201
|
+
|
|
202
|
+
| Fichero:línea | Cambio | Bloq |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `server/providers/gemini-adapter.ts` (nuevo) | El adapter completo (§3). | **sí** |
|
|
205
|
+
| `server/providers/index.ts:11-14,17` | import + `register(geminiAdapter)` + re-export. | **sí** |
|
|
206
|
+
| `server/pricing.ts:44-50` | 4 filas `'gemini:<model>'` (§2). Sin ellas coste=NULL. | **sí** |
|
|
207
|
+
| `server/project-router-tickets.ts:1540-1556` (ai-edit) | dispatch hardcoded `if(provider==='codex'){binary='codex'...} else {binary='claude'...}` → **un proyecto gemini spawnearía 'claude' (binario equivocado)**. Reemplazar por `adapter.binary` + `adapter.buildArgs` (patrón ya usado en quick-spec línea 341). | **sí** |
|
|
208
|
+
| `server/project-router-tickets.ts:420,610,1022,1148` | bloques `if(provider==='codex')` sin hermano gemini → generalizar o añadir arm gemini. | **sí** |
|
|
209
|
+
| `server/setup-manager.ts:697-700,1306-1307` | regex allow-list `m[1]==='claude'\|\|m[1]==='codex'` **descarta 'gemini' silenciosamente** → install-config cae a claude. Añadir `'gemini'`. **Fallo silencioso de alto riesgo.** | **sí** |
|
|
210
|
+
| `server/setup-manager.ts:495-503` | `computeSummary` branch codex → hermano gemini si el summary difiere. | menor |
|
|
211
|
+
| `server/setup-prerequisites.ts:405-423` | switch de install-hint: añadir `case 'gemini':` (URL/comando install Gemini CLI). | menor |
|
|
212
|
+
| `server/agent-refine-manager.ts:73,79` | ensanchar `provider?: 'claude'\|'codex'`. (validateAgentBody ya skip non-claude). | **sí** (compile) |
|
|
213
|
+
| `server/chat-manager.ts:124,132,332` | ensanchar `provider?: 'claude'\|'codex'` ctor + cast persistido. Gates `adapter.id==='claude'` (scope/userMcp) ya rutean gemini como codex (OK). | **sí** (compile) |
|
|
214
|
+
| `server/result-event.ts:98-117` | `normaliseResultEvent` legacy shim: gemini cae al branch codex `else` (mis-parsea usage). **Preferir `adapter.extractResult`** (path moderno). Ensanchar union por si se usa. | menor |
|
|
215
|
+
| `client/src/types/context-scope.ts:62-67` | añadir filas de coste gemini (cliente). | menor |
|
|
216
|
+
| `client/src/components/analytics/ProviderBreakdownCard.tsx:9-17` | `PROVIDER_LABEL`/`PROVIDER_ACCENT`: añadir `gemini` (label + accent, p.ej. `bg-accent-success`). | menor |
|
|
217
|
+
| `client/src/components/Navbar.tsx:26-43` | badge: añadir `=== 'gemini'` (label/color), sino cae a 'no CLI' rojo. | menor |
|
|
218
|
+
| `client/src/components/SetupWizard.tsx:63-76` | heurísticas modelId sonnet/haiku/opus son claude-specific → branch gemini si el configure debe mostrar modelo. | menor |
|
|
219
|
+
| `server/desktop-router.ts:28-35,155-157,229-234` | gate beta: añadir `SPECRAILS_GEMINI_BETA` paralelo a `SPECRAILS_CODEX_BETA`. Generalizar el refuse hardcoded `providers.includes('codex')` (línea 229). | menor (necesario para el gate) |
|
|
220
|
+
| `docs/adding-a-provider.md` (**no existe en disco**) | **Crearlo** — CLAUDE.md lo referencia pero falta. Documentar el patrón "un fichero + una entrada" usando gemini como ejemplo. | menor |
|
|
221
|
+
|
|
222
|
+
**NO tocar (verificado, evitar over-edit):** `core-compat.ts` (registry-driven), `provider-selection.ts` (membership; solo necesita `CliProvider` ensanchado), `spawn-lifecycle.ts` (adapter-driven, cero literales), `result-event.ts` `finaliseInvocationResult` (id-driven), `registry.ts`, `queue-manager._resolveJobAdapter`, `COALESCE(provider,'claude')` en ai-invocations/spending/db (solo backfill de NULLs legacy), `contract-refine-runner.ts`/`smash-runner.ts` (claude-only intencional).
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 6. Matriz de feature-parity
|
|
227
|
+
|
|
228
|
+
| Feature | ¿Gemini lo soporta? | Qué haría falta para paridad |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| **Jobs/rails (compila + corre)** | **Degradado/Bloqueado** | Compila vía adapter. **BLOQUEANTE para rails reales**: `queue-manager.ts:976-981` pasa el slash `/specrails:implement #N` crudo a Gemini en el `else` final — Gemini NO entiende slash-commands de Claude (Codex los traduce a `$skill`). Paridad: branch gemini que mapee slash→forma que entiendan las skills de specrails-core en Gemini. |
|
|
231
|
+
| **Explore Spec (multi-turno)** | **Nativo (degradado)** | `--resume` headless + `--session-id` pre-asignado funcionan. Spawn-per-turn con `-p`. Sin pérdida básica. |
|
|
232
|
+
| **— persistent-stdin Explore** | **Bloqueado** | `persistentStdin:false` → spawn-per-turn (default OFF de todas formas). Requiere stdin stream-json long-lived que Gemini no documenta. |
|
|
233
|
+
| **— tool-gating (Explore/Quick scope)** | **Degradado (default-safe)** | Gates `adapter.id==='claude'` → gemini recibe args vacíos (sin restricción; usa su propio `--approval-mode plan/yolo`). Paridad: branch gemini en `toolFlagsForScope` (`context-scope.ts`) mapeando ContextScope → flags Gemini. |
|
|
234
|
+
| **— MCP per-spec (`.mcp.json`)** | **Degradado** | Gemini usa `.gemini/settings.json` `mcpServers`, NO el `.mcp.json` de claude ni `--mcp-config` inline. Paridad: escribir `.gemini/settings.json` en la cwd o `--allowed-mcp-server-names`. |
|
|
235
|
+
| **— user-MCP (My approved MCPs)** | **Degradado (default-safe)** | `buildUserMcpArgs` devuelve `[]` para non-claude. Gemini lee su MCP global desde `~/.gemini/settings.json` (como codex con `~/.codex`) → no-op correcto, sin pérdida. |
|
|
236
|
+
| **Quick spec** | **Nativo** | spawn one-shot vía adapter; coste por rate-card. |
|
|
237
|
+
| **Agent profiles** | **Bloqueado (paridad codex)** | `CLAUDE_ONLY_SECTIONS=['agents']`, `profileEnvSupport` vestigial. Oculto por intersección. Paridad: soporte por sección en `provider-capabilities.ts:44-71` + `projectSupportsProfiles` por-provider. |
|
|
238
|
+
| **Integrations/plugins (MCP/Serena)** | **Bloqueado (paridad codex)** | Manifest Serena omite `providerSupport` → claude-only. Paridad: añadir `'gemini'` a `providerSupport` + entry MCP bajo `.gemini/settings.json`. |
|
|
239
|
+
| **SMASH** | **Bloqueado (paridad codex)** | `isSmashCapable→'claude'`. `smash-runner.ts` spawnea `claude` directo sin adapter. Paridad: reescribir runner sobre `getAdapter()` + prompt/parser Gemini + flip del gate. |
|
|
240
|
+
| **Contract Refine** | **Bloqueado (inherentemente anthropic en su forma actual)** | `--resume` a sesión Claude + slash `/specrails:contract-refine`. Paridad parcial vía el path no-resume (re-seed por system prompt, como `runContractRefineForQuick`) + de-hardcodear `getAdapter('claude')`. |
|
|
241
|
+
| **Ultracode interactivo** | **Bloqueado (paridad codex)** | `mode==='ultracode' && provider!=='claude'→400`; `isUltracode=adapter.id==='claude'`. Paridad: quitar el 400 para gemini, generalizar el branch ultracode-prompt, `persistentStdin:true` para el interactivo. Mecanismo no-anthropic; factible. |
|
|
242
|
+
| **Analytics/coste** | **Nativo (estimado)** | `nativeCostUsd:false` + filas pricing → coste estimado, `estimated=true`. Filtros engine ya data-driven. |
|
|
243
|
+
| **OTEL/telemetry** | **Nativo (MEJOR que codex)** | `nativeOtelEnv:true` → env-injection (no bridge). Solo parametrizar el enable-var en `buildTelemetryEnv`. Caveat correlación (§4). |
|
|
244
|
+
| **Terminal CLI launch** | **Nativo** | `CliLaunchMenu` itera providers; aparece tras ensanchar union + wiring binario. |
|
|
245
|
+
|
|
246
|
+
**Problema de la intersección binaria `=== 'claude'`**: `sectionVisibleForProviders` **oculta una sección si CUALQUIER proveedor instalado no la soporta**. En un proyecto mixto claude+gemini, Agents/SMASH/etc. desaparecen porque gemini no las declara — igual que codex hoy. **Si se generalizara `provider-capabilities.ts`** (soporte por-sección por-proveedor en vez del `Set` claude-only), Gemini *ganaría*: Agent Profiles (solo env `SPECRAILS_PROFILE_PATH`, agnóstico), tool-gating, user-MCP, file-summary cheap-model — todas mecanismo-dependientes. Solo Contract Refine es inherentemente anthropic. **Decisión v1**: paridad-codex (ocultar), generalización diferida a follow-up.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 7. Riesgos y mitigaciones
|
|
251
|
+
|
|
252
|
+
| Riesgo | Mitigación |
|
|
253
|
+
|---|---|
|
|
254
|
+
| **Pin de versión pre-1.0** (cambia a diario; nightly 0.48) | `minCliVersion='0.11.0'` (piso validado de stream-json). `detectInstalled` surface meetsMinimum + upgrade hint. Documentar que los ids preview rotan. |
|
|
255
|
+
| **Drift del stream-json** (bugs #9281/#11184/#9009) | Esos bugs son del `--output-format json` (single-object `response`), **NO del stream-json** (usa message deltas, sin `response`). #9009 (flag desconocida) cerrado, afecta <0.6 — el pin 0.11 lo evita. **Golden-snapshot test**: fixtures NDJSON bajo `__fixtures__/` verbatim del `stream-json-formatter.test.ts` upstream, aserción `toEqual` sobre el AdapterEvent[]. |
|
|
256
|
+
| **Scope por cwd-hash en resume** | Sesiones scopeadas por `getProjectHash(cwd)`. **Siempre spawn desde `project.path`** (ya lo hace el host). Resume cross-cwd no encuentra la sesión → host-controllable. Pre-asignar `--session-id <UUID>` para no depender de parsear init en builds <2025-12-04. |
|
|
257
|
+
| **Exit code 53** (turn limit, no quota) | Mapear 53 = "cap de turnos `maxSessionTurns`" distinto de error general. 42 = bad input/args. Resto non-zero → tratar como exit 1 (general/API). Quota/429/403 colapsan a exit 1. |
|
|
258
|
+
| **Auth headless** | **Presetear `GEMINI_API_KEY`** en el env de spawn → `getAuthTypeFromEnv()` devuelve `USE_GEMINI`, `validateAuthMethod` pasa, sin browser. Alt CI: `GOOGLE_GENAI_USE_VERTEXAI=true`+`GOOGLE_CLOUD_PROJECT`+`GOOGLE_CLOUD_LOCATION`+`GOOGLE_APPLICATION_CREDENTIALS`. **No setear `security.auth.enforcedType`** conflictivo. Sin var de auth → `process.exit(FATAL_AUTHENTICATION_ERROR)` (no browser). El host necesita un flujo para que el usuario provea la API key (Settings/AddProject). |
|
|
259
|
+
| **Drift OTEL** (correlación `session.id` vs `specrails.job_id`) | Telemetry OFF por defecto → no bloquea v1. Al activar: validar si Gemini propaga `OTEL_RESOURCE_ATTRIBUTES`; fallback correlación por `--session-id` de spawn-time. |
|
|
260
|
+
| **Bug OTLP/HTTP #15581** (POST a `/` no `/v1/{signal}`) | Usar `GEMINI_TELEMETRY_OTLP_PROTOCOL=grpc` (default :4317, well-tested), evitar HTTP. |
|
|
261
|
+
| **Coste >200k tokens infra-estimado** | Aceptado en v1 (tier ≤200k). Analytics marca `estimated`. Follow-up: tiering en `PriceEntry`. |
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 8. Plan de tests + coverage
|
|
266
|
+
|
|
267
|
+
CI exige **80% server** (lines/functions/statements, 70% branches) y **80% client** (lines/statements, 70% functions). Cada fichero tocado debe llegar.
|
|
268
|
+
|
|
269
|
+
**Tests nuevos server:**
|
|
270
|
+
- `server/providers/gemini-adapter.test.ts` (espejo de `codex-adapter.test.ts`):
|
|
271
|
+
- `capabilities` exactos (8 campos).
|
|
272
|
+
- `buildArgs` por cada uno de los 9 SpawnActions (línea exacta; resume throw si `!sessionId`; chat-stream throw; fold cuando `systemPromptArg=false`; `extraArgs` appendeado).
|
|
273
|
+
- `parseStreamLine`: fixtures NDJSON bajo `server/providers/__fixtures__/gemini-*.ndjson` (init/message/tool_use/tool_result/error/result/línea-vacía/parse-fail), aserción del AdapterEvent[].
|
|
274
|
+
- `extractResult`: stats → tokens_in/out/cache_read, num_turns, session_id, `total_cost_usd` undefined.
|
|
275
|
+
- `detectInstalled`: mock de `execSync` (no instalado / version OK / version < min / probe-fail / timeout >3s → installed:false).
|
|
276
|
+
- `server/pricing.test.ts`: 4 filas `gemini:*`, `estimateCostUsd('gemini',model,usage)` correcto; clave ausente → null; semántica cached (no doble-conteo).
|
|
277
|
+
- `server/provider-selection.test.ts`: `isProviderEnabled`/`resolveProvider`/`validateRequestedProvider` aceptan `'gemini'`; multi-provider claude+gemini.
|
|
278
|
+
- `server/providers/registry.test.ts`: `getAdapter('gemini')`/`hasAdapter`/`listAdapters` incluye gemini tras import de index.
|
|
279
|
+
|
|
280
|
+
**Tests nuevos client:**
|
|
281
|
+
- `ModelSelector`/`ChatInput`: provider `'gemini'` muestra `GEMINI_MODELS` (no Codex).
|
|
282
|
+
- `AddProjectDialog`: checkbox gemini cuando `/available-providers` lo devuelve; `noProviderAvailable` iterado.
|
|
283
|
+
- `provider-capabilities.test.ts`: `providerLabel('gemini')`, `sectionVisibleForProviders` oculta Agents/SMASH en proyecto con gemini (paridad codex).
|
|
284
|
+
- `ProviderBreakdownCard`/`Navbar`: label+accent gemini.
|
|
285
|
+
|
|
286
|
+
**Gate `SPECRAILS_GEMINI_BETA`**: test en `desktop-router.test.ts` — con `=0`, `/available-providers` devuelve `gemini:false` y `POST /projects` con `providers:['gemini']` se rechaza; default/`1` → habilitado. Espejo de `SPECRAILS_CODEX_BETA`.
|
|
287
|
+
|
|
288
|
+
**Cumplir 80%**: el adapter es lógica pura (sin I/O salvo `detectInstalled` mockeado) → fácil 100%. Cubrir los 9 cases de `buildArgs` garantiza branch coverage. Las filas pricing y los branches `=== 'codex'` generalizados necesitan un test que ejercite el path gemini (ai-edit con provider gemini → `adapter.binary='gemini'`). Excluir solo lo Tauri-only inalcanzable en jsdom, documentando inline; **nunca para enmascarar tests faltantes**.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 9. Rollout por fases
|
|
293
|
+
|
|
294
|
+
1. **PR-A (generalización previa)** — ensanchar `CliProvider`/`ProviderId`/las ~12 uniones `('claude'|'codex')[]`, desramificar `/available-providers` y `spec-models.ts`/`ModelSelector`/`ChatInput` a lookups por id. **Sin Gemini todavía**; comportamiento idéntico. Mergeable y testeable solo. Reduce el blast-radius del PR-B.
|
|
295
|
+
2. **PR-B (el adapter)** — `gemini-adapter.ts` + `register` + filas pricing + branches gemini (ai-edit dispatch, setup-manager regex, install-hint) + labels/accents + `SPECRAILS_GEMINI_BETA`. **Gemini detrás del gate beta**, default ON con kill-switch.
|
|
296
|
+
3. **Beta-gated validation** — probar end-to-end: detect, add-project con gemini, Quick spec, Explore multi-turno (resume), Analytics coste, terminal launch. Validar exit 53/42, auth `GEMINI_API_KEY`. **NO** habilitar rails reales hasta resolver la traducción slash-command (§6, bloqueante) — gemini sirve para spec/explore/quick en esta fase.
|
|
297
|
+
4. **Docs** — crear `docs/adding-a-provider.md` (falta) con gemini como ejemplo, y `docs/gemini.md` (espejo de `docs/codex.md`) con setup de API key.
|
|
298
|
+
|
|
299
|
+
**Primer commit recomendado:** PR-A paso atómico — ensanchar `server/desktop-db.ts:10` `CliProvider` para incluir `'gemini'` y convertir el ternario de `server/spec-models.ts:1-37` `getModelsForProvider` en lookup `Record<provider, SpecModelOption[]>`. Desbloquea la cadena de tipos sin tocar comportamiento y deja la base lista para registrar el adapter.
|
|
300
|
+
|
|
301
|
+
**Puntos `[UNCERTAIN]` pendientes (no inventados):** (a) `OTEL_RESOURCE_ATTRIBUTES` propagation en Gemini — validar empíricamente antes de confiar para correlación job/project; (b) semántica de `cachedContentTokenCount` como subconjunto de input — fijar por test de pricing; (c) `mcpRegistration` exacto en la versión bundleada — verificado que escribe a settings.json (`project-json`), confirmar en build. Ninguno bloquea v1 (spec/explore/quick); todos tienen fallback.
|