Tabla de contenidos
- Qué es Forge Mobile en una frase
- Arquitectura cliente / servidor
- Shell de UI -- 3 modos
- Chat send + branch por pair state
- Como el telefono obtiene respuestas (relay)
- Voz -- solo en la APK nativa
- LAN discovery (mDNS + polling)
- State channel cross-device
- Pairing QR + 6 dígitos
- Storage adapter (3 scopes)
- Transcript persistence
- SSE client RN-compatible
- BYOK keys (locales en el escritorio)
- SettingsPanel (5 cards)
- NAC-3 attrs + a11y verbs
- Permisos + metadata (app.json)
- i18n 10 idiomas
- Lo que mobile NO incluye
- Estado del install + EAS profiles
- Tests + typecheck
序Qué es Forge Mobile en una frase
Forge Mobile es el companion movil de Yujin Forge Desktop, no un reemplazo. Tu laptop sigue siendo el lugar donde Forge escribe codigo, ejecuta tests y deploya. El telefono es el control remoto: maneja la sesion del escritorio a traves del relay HITO 4 -- espeja el tablero vivo (grafo / workflows / pendientes / documentos) y puede pedir turnos de chat que el escritorio ejecuta con TUS claves locales. Viene en dos formas: la PWA (web app, sin tienda, sin microfono) y la app nativa (APK), que agrega voz on-device. Las BYOK keys quedan en el vault del escritorio -- nunca viajan al telefono.
Bundle ID + package Android: app.yujin.forge.mobile.
Stack: Expo + React Native + expo-router. La APK se construye en
GitHub Actions (.github/workflows/mobile-android.yml -> release
mobile-latest); la PWA se exporta con
expo export -p web y la sirve el worker HITO 4 ([assets]).
層Arquitectura cliente / servidor
Mobile NO corre código del adopter, NO ejecuta builds, NO toca el filesystem del laptop. Es un cliente puro al worker HITO 4 + descubrimiento LAN del laptop:
| Capa | Donde vive | Propósito |
|---|---|---|
| App | apps/forge-mobile/ | Expo binding del shell + adapters + assets sumi-e. |
| Cross-platform package | packages/nac3-rn/ | Logica pura compartible: pairing / relay_client / a11y_adapter + componentes RN del shell. Una sola base para PWA (web) y APK (nativo). |
| Worker HITO 4 | Cloudflare Workers (packages/hito4-server/) | Relay control remoto telefono<->escritorio (/v1/relay/<handle>) + flows (/v1/flows/*) + license + cost meter + semantic graph + sirve la PWA ([assets]). Las BYOK keys NUNCA llegan al worker (PND-099: /v1/keys-sync removido; no guarda nada que descifrar). |
| Laptop Forge | Tu máquina, yf chat | Anuncia _yujin-forge._tcp.local. en la LAN para que mobile lo descubra. |
形Shell de UI -- 3 modos
Misma convención que Forge Desktop: ladder lineal
globito ↔ mini ↔ full. Skipping permitido por voice command.
Maquina de estado pura en packages/nac3-rn/src/modes.ts
(sin React, sin RN -- testeable desde cualquier host).
- Globito: burbuja flotante; solo el input. Tap para abrir mini.
- Mini: chat + historial scrollable; sin canvas, sin voz.
- Full: canvas + voz + chat + previews. El botón de voz despacha el voice loop reducer.
Boot en modo Globito por default (apps/forge-mobile/app/index.tsx:38).
capabilitiesFor(mode) reporta qué UI render por modo:
showInput / showHistory / showCanvas / showVoiceButton.
話Chat send + branch por pair state
User type -> send(text) callback. La función send_classifier
ramifica por pair state (null = booting, false = unpaired,
true = paired):
paired === null: echo de la pregunta + nota "verificando".paired === false: echo + system msg pidiendo pairing.paired === true: POST a HITO 4 viaaskBrain.
Lógica pura, sin React Native, vive en
apps/forge-mobile/components/send_classifier.ts.
Tests en tests/mobile-send-classifier.test.ts.
ChatBubble: el componente visual
Cada mensaje renderiza con un ChatBubble que muestra:
- Role alignment: user (derecha indigo) / assistant (izquierda crema) / system (centro dashed).
- Tier badge:
ecoofrontieren assistant. - Escalated chevron: cuando el brain selection escaló mid-turn.
- Tool rounds chip: cuenta de verbos NAC-3 invocados.
- Error border + retry pressable cuando el envío falló.
Source: apps/forge-mobile/components/ChatBubble.tsx (z.178).
脳Como el telefono obtiene respuestas (via el relay)
El telefono no corre un brain ni guarda claves. Escribis (o
hablas, en la APK) y el cliente publica un comando de chat al
relay del worker (/v1/relay/<handle>). Tu escritorio
-- que es la fuente canonica -- lo toma, ejecuta el turno con TU brain y TUS
claves locales (Anthropic / OpenAI / Google AI / DeepSeek, o tu plan), y
devuelve la respuesta por el mismo relay. El telefono es un terminal delgado;
el trabajo real pasa en la maquina.
- Binding por turno: cada comando lleva el
project_slugque el telefono esta mostrando; el escritorio ejecuta ese turno contra ese proyecto -- nunca contra otro -- aunque haya cambios en paralelo. - Una sola instancia ejecuta: si corren varios
yf chat, solo el dueno del proyecto taggeado atiende el comando (sin ejecucion duplicada). - Auth HMAC por handle (PND-047): el telefono firma con la clave derivada en el pairing; el worker solo enruta el sobre, no lo entiende.
- Dos sesiones: "notebook" (espeja y comparte la sesion del escritorio) y "propia" (un hilo aparte del telefono).
Source: src/relay/command_consumer.ts +
src/chat/server.ts (consumer + binding) +
packages/nac3-rn/src/relay_client.ts.
声Voz -- solo en la app nativa (APK)
La voz vive en la app nativa (APK), no en la PWA. Usa el
reconocedor del sistema operativo (expo-speech-recognition:
Android SpeechRecognizer / iOS Speech framework) para STT y
expo-speech para TTS -- gratis, sin claves, y nada de
audio sale del telefono (lo procesa el pipeline del SO). Apretas para hablar,
soltas para enviar; el texto reconocido se manda como un turno normal.
La PWA no tiene microfono. El dictado del navegador
(webkitSpeechRecognition) no captura en una web app instalada de
Android (limitacion de Chromium, no es el permiso), y no hay un camino de STT
keyless por servidor (el STT del worker es BYOK y el relay no transporta audio).
Por eso el boton de microfono solo aparece en la APK.
- Default keyless: el reconocedor del SO -- nada que cargar, nada que viaje.
- Premium opcional (BYOK on-device): podes cargar una clave STT/TTS (Google / OpenAI / ElevenLabs) que queda en el secure-store del telefono y va directo al proveedor -- no pasa por el worker ni por el escritorio.
- iOS: build nativo a pedido (mismo adapter, Speech framework).
Source: apps/forge-mobile/components/voice_adapter.ts (nativo)
+ voice_adapter.web.ts (web, sin mic).
Permisos: RECORD_AUDIO (Android), NSMicrophoneUsageDescription (iOS).
網LAN discovery (mDNS + polling)
Tu phone descubre tu laptop dentro de la LAN sin servidor intermedio:
- mDNS / Bonjour via
react-native-zeroconfcomo peer dependency opcional. Servicio buscado:_yujin-forge._tcp.local. - Polling fallback cuando zeroconf no está instalado:
pollingFallback({ targets, intervalMs:5000, probeTimeoutMs:1500 })con HTTP HEAD a manual targets. - iOS Bonjour entitlement declarado en
app.json:NSBonjourServices: ["_yujin-forge._tcp"]+NSLocalNetworkUsageDescription. - Android multicast permiso:
CHANGE_WIFI_MULTICAST_STATE(sin esto el chip mDNS no puede leer).
El project stripe (chips horizontales) muestra cada laptop discovered con
slug + source + host. Empty state cuando no hay laptops:
chip "no-laptops-yet" (no crash, no spinner perma).
Source: packages/nac3-rn/src/lan_browse.ts +
apps/forge-mobile/components/lan_adapter.ts.
同State channel cross-device
Pablo's use case: "I'm coding on the laptop, I pick up my phone,
y el phone ya sabe en qué proyecto estoy." El state channel sincroniza
el active_project_slug automáticamente.
- Long-poll a
/v1/state/<user_handle>-- el worker mantiene el último StateMessage en D1 + holds la conexión. - Solo opens cuando paired: sin secret -> no channel.
- Publish en cada local pick:
pickProject(slug) -> handle.publish({ active_project_slug: slug }). - Ignora echos propios: cada device firma con
source_device; el client filtra los mensajes que origino el mismo device id. - Exponential backoff en disconnect (1s ceiling 30s).
Source: packages/nac3-rn/src/state_channel.ts.
合Pairing QR + 6 dígitos
Cómo transferir el secret del CLI al phone sin contraseñas largas ni cloud:
- CLI genera un código de 6 dígitos (1e6 entropy, fácil leer en voz).
- CLI arma una
PairingPayload={ user_handle, server_api_key, hito4_base_url, expires_at }y la encripta con AES-GCM + clave derivada del código via PBKDF2 (200k iters SHA-256, salt random per-pairing). - CLI emite el blob encriptado + el salt + el hash del código como JSON QR.
- Mobile escanea QR, te pide el código de 6 dígitos, re-deriva la clave, desencripta. Si tipeás mal el código, el AEAD tag falla y el blob ni se abre (UI "incorrect code").
Defensas anti-bruteforce: 5 min expires_at por pairing. El CLI tear-down después de 1 éxito O 3 fallos.
Crypto Web Crypto only -> corre en RN + Node + el laptop CLI.
Cross-impl validada por scripts/sync_pairing.mjs --check en
lefthook pre-commit (PND-008 / z.169).
Source: packages/nac3-rn/src/pairing.ts
+ src/core/pairing.ts (CLI side).
Tests cross-impl: tests/pairing-cross-impl.test.ts.
蔵Storage adapter (3 scopes)
Storage adapter centralizado para mantener tres tipos de datos separados:
| Scope | Backend | Qué guarda |
|---|---|---|
secret | expo-secure-store (device keychain) | HMAC key del HITO 4, user handle, JWT/PAT. Acceso bloqueado por jailbreak / root. |
setting | In-memory hoy (AsyncStorage swap es 1 línea) | Provider preference, language, voice mode, STT/TTS picks. |
session | In-memory | Estado volátil del session: voice snapshot, draft text, history en RAM. |
API: get(scope, key) -> string|null, set, delete, list.
List de secrets retorna [] -- expo-secure-store no tiene list primitive,
la app mantiene su propio index en setting cuando necesita.
Slot validation: alfanumérico + ., _, -. Hard-reject loud cualquier otra cosa.
Prefix yfsecure. en keys del SecureStore para auditar.
Source: apps/forge-mobile/components/storage_adapter.ts
+ packages/nac3-rn/src/persistence.ts (contract).
録Transcript persistence
Persistencia local de conversaciones cross-restart de la app:
- Una row por
project_slug-- keyyujin-forge.transcript.v1.<slug>. - AsyncStorage-compatible: callers pasan el AsyncStorage real
cuando el dep está instalado; default es
InMemoryTranscriptBackendpara tests + fresh installs. - Serialised writes per project:
PENDING_WRITESPromise chain previene races de 2 appends rápidos (matchea el patrón de desktopingest_session). - Cap configurable (default 500 turnos por proyecto): cuando hace overflow, drop oldest.
- Filtra mensajes corruptos en hydrate: role inválido,
tsfaltante,contentnon-string. - Metadata por turno:
brain_tier(eco/frontier) +doc_loadedflag para sticky-tier inertia.
API: read / append / clear / listProjects / flush.
Source: apps/forge-mobile/components/transcript_store.ts (z.178).
Tests: tests/mobile-transcript-store.test.ts (12 casos).
流SSE client RN-compatible
React Native NO soporta fetch streaming nativamente
(response.body es un Blob, no un ReadableStream). Para consumir
text/event-stream usamos XHR en modo incremental:
- XHR-driven:
XMLHttpRequest+ listener enonreadystatechangeque leeresponseText.slice(lastReadLen)en cada chunk arrival (readyState===3). - Frame parser puro:
parseSseChunks(buf) -> {events, leftover}. Grammar EventSource standard: event / data / id / retry / comment lines. CRLF + LF + CR endings soportados. - Last-Event-ID header en cada reconnect para resumir el stream sin perder eventos.
- Exponential backoff reconnect: initial 1s, ceiling 30s,
cap configurable de
maxRetries. - HTTP errors (≥400 en
readyState===2) ->onError({kind:'http', status})+ reconnect (o stop simaxRetries===0). .close()para stop perma +.isOpen()para checkear.- Pluggable xhrFactory -- los tests inyectan fake XHR.
Source: apps/forge-mobile/components/sse_client.ts (z.178).
Tests: tests/mobile-sse-client.test.ts (11 casos).
鍵BYOK keys -- locales en el escritorio (no sincronizan al telefono)
Las API keys del usuario viven solo en el vault del escritorio
(cifrado local, AES-GCM). No se sincronizan al telefono. El endpoint
/v1/keys-sync fue removido (PND-099) como endurecimiento de
seguridad: el worker no guarda ningun blob de claves -- el operador no tiene
nada que descifrar.
- El telefono es keyless: maneja la sesion del escritorio via el relay, y el escritorio ejecuta los turnos con SUS claves locales.
- Voz en la APK: reconocedor del SO (sin claves). Opcional, una clave STT/TTS premium que queda en el secure-store del telefono y va directo al proveedor -- nunca al worker ni al escritorio.
- Una sola vez: cargas tus claves en el escritorio con
yf keys setup; nunca las re-tipeas en el telefono.
Source: src/vault/ (vault del escritorio) +
packages/hito4-server/src/worker.ts (PND-099: keys-sync removido).
設SettingsPanel (5 cards)
Settings full-screen scrollable. 5 cards verticales con paridad selectiva respecto al desktop:
| Card | Contenido | Mutators |
|---|---|---|
| Plan hero | status (paid / trial / expired / unknown) + user_handle + expires_at. Color border por estado. | — |
| Brain provider (BYOK) | 4 provider chips: Anthropic, Google AI, OpenAI, DeepSeek. Key mask (sin plaintext) + edit button. | onSetBrainProvider, onEditBrainKey |
| Voice | 3 toggles: modo (mic/manual), STT (browser/google/whisper), TTS (browser/google/elevenlabs). | onSetVoiceMode, onSetStt, onSetTts |
| Language | 10 chips: es / en / pt / fr / de / it / ja / zh / hi / ar. | onSetLanguage |
| About | Forge version + handle + url. | — |
Pure presentational + storage-driven. testIDs en cada control para automation NAC-3 cross-target.
Source: apps/forge-mobile/components/SettingsPanel.tsx (z.178).
援NAC-3 attrs + a11y verbs
Cross-target attrs
Mismo data-nac-id en web tiene mismo testID en RN. El helper
nacId('login.submit') emite ambos. Roles se mapean:
button / link / text / image / header / tab / switch / none | -> | accessibilityRole |
label | -> | accessibilityLabel |
hint | -> | accessibilityHint |
Source: packages/nac3-rn/src/rn_attrs.ts.
A11y verbs runtime (PND-009 / z.170)
Mapea los verbos yujin.a11y.announce/focus/label a las APIs nativas:
announce->AccessibilityInfo.announceForAccessibility(text)+ optional TTS.focus->AccessibilityInfo.setAccessibilityFocus(nodeHandle)via slug registry.label-> label override registry; consumers re-leen viauseA11yLabel.
Pure adapter (packages/nac3-rn/src/a11y_adapter.ts)
+ binding RN (apps/forge-mobile/components/a11y_adapter.tsx)
con hooks useA11ySlug(slug, ref) + useA11yLabel(slug, fallback).
許Permisos + metadata (app.json)
| Plataforma | Permiso / config | Razón |
|---|---|---|
| iOS | NSMicrophoneUsageDescription | Voice loop (record + transcribir). |
| iOS | NSLocalNetworkUsageDescription | LAN discovery (mDNS browse). |
| iOS | NSBonjourServices: ["_yujin-forge._tcp"] | Entitlement para anunciar / leer el servicio mDNS. |
| iOS | supportsTablet: true | iPad-friendly. |
| Android | RECORD_AUDIO | Voice loop. |
| Android | INTERNET | HTTPS al worker HITO 4. |
| Android | ACCESS_NETWORK_STATE | Detectar online/offline + Wi-Fi. |
| Android | CHANGE_WIFI_MULTICAST_STATE | mDNS browse (sin esto el chip no lee multicast). |
| Ambos | scheme: "yujin-forge" | Deep links + expo-router URLs. |
Assets sumi-e (todos 1024×1024 opaque corners, iOS App Store
shippeables):
assets/icon.png (calligraphy del kanji 声 "voz" con chop Yujin),
assets/adaptive-icon.png (circular crema para Android),
assets/splash.png (1024×1536 portrait con brushstroke).
Splash background: #F8F6F0 (crema sumi-e). Same brand palette que el laptop.
国i18n 10 idiomas
Mobile comparte los mismos catálogos que el Desktop:
src/i18n/catalogs/<lang>.json. 10 locales soportados:
es / en / pt / fr / ja / zh / hi / ar / de / it.
- SettingsPanel ofrece switcher in-app (10 chips, ver § Settings).
- Mismo contrato
tForLang(lang, key, params)que el panel del desktop. - Lint pre-commit garantiza que cada key en
es.jsonexiste en los otros 9 locales.
不Lo que mobile NO incluye
Decisiones de diseño, no olvidos. Mantienen mobile delgado y fuerzan laptop-first para tareas sensitive:
| Feature | Por qué no en mobile |
|---|---|
| Vault management completo | Mobile solo expone los slots necesarios para chat + voz. Slot catalog + import-legacy + redactor viven laptop-only. |
| Limits config editor | Las limits viven en el laptop (~/.yujin-forge/limits.json); mobile las hereda por state channel. |
| Audit log preview | La jsonl del audit log vive en el laptop; mobile no tiene acceso al disco del laptop. |
| Danger Zone | Intencionalmente laptop-only por seguridad. El device más liviano nunca puede auto-aprobar destructivos. |
| VAD tuning wizard | En la APK el reconocedor del SO maneja la deteccion de voz; no hay tuning de VAD en el telefono. Los 9 sliders + freq-band toggle viven en el panel del desktop. |
| First-run wizard de licencia | Mobile asume que el pairing ya está hecho desde el laptop -- ahí se eligió Plan vs Console BYOK. |
| Pending registry CLI | Mobile no edita pendings -- es herramienta de desarrollo del producto. |
| Brain matrix QA harness | Mobile NO corre el harness. Es laptop-only. |
配Distribucion -- APK por GitHub Actions + PWA por el worker
Dos caminos, ambos en vivo hoy (EAS ya no se usa para los builds de test):
- PWA:
expo export -p webproduceapps/forge-mobile/dist, que el worker HITO 4 sirve via[assets]. Abris la URL en el telefono, "Agregar a inicio", y vinculas. Sin tienda, sin microfono. - APK (Android, con voz): el workflow
.github/workflows/mobile-android.ymlcompila la APK en la nube en cada push que tocaapps/forge-mobile/**y la publica al releasemobile-latest(descarga directa, sideload). La distribucion via Play Store / TestFlight sigue siendo trabajo futuro (abajo).
pnpm --filter @yujin/forge-mobile typecheck pasa en HEAD.mobile-android.yml compila la APK arm64 en un runner gratis de GitHub en cada push de apps/forge-mobile/** y la publica al release mobile-latest. Reemplaza EAS para los builds de test.expo export -p web -> apps/forge-mobile/dist, servido por el worker HITO 4 via [assets]. Instalable desde el navegador del telefono.production scaffold listo con placeholders TODO en eas.json; .gitignore cubre el service account JSON.EAS build profiles (eas.json) -- legacy
Los builds de test ya NO usan EAS: la APK se compila en GitHub Actions (arriba). Estos profiles quedan para una eventual ruta de store (Play / TestFlight).
| Profile | Distribution | Channel | Android format | Uso |
|---|---|---|---|---|
internal | internal | internal | APK | Sideload testing en device físico. |
preview | internal | preview | APK | Candidates antes de promover a TestFlight / Play internal. |
production | internal | production | APK | Direct-install legacy (apk sideload). |
production-store | store | production | AAB (autoIncrement) | Play Store. Required format para submit. |
Runbook completo de Phase 0 -> 3 en el repo:
docs/MOBILE_DISTRIBUTION_RUNBOOK.md.
検Tests + typecheck
Tests pure-logic corren bajo Node (sin RN runtime):
| Archivo | Casos | Cubre |
|---|---|---|
tests/mobile-transcript-store.test.ts | 12 | read / append / clear / listProjects / cap eviction / corruption filter / serialised writes. |
tests/mobile-sse-client.test.ts | 11 | parseSseChunks 8 casos + connectSse loop 3 casos (chunks, HTTP error, close()). |
tests/mobile-managed-client.test.ts | 5 | happy path, tool roundtrip, auth-missing error, 5xx server error, maxToolRounds bound. |
tests/nac3-rn-a11y-adapter.test.ts | 13 | announce / focus / label + slug registry + label override broadcaster (PND-009). |
tests/mobile-send-classifier.test.ts | — | Branch por pair state + error classifier (alpha.49 era). |
Mobile typecheck (pnpm --filter @yujin/forge-mobile typecheck)
está verde en HEAD.
Generado leyendo: apps/forge-mobile/{app,components,assets,app.json,eas.json},
packages/nac3-rn/src/*, + docs/MOBILE_CAPABILITIES.md.
Cero invento -- cada feature listada existe en el repo.
Ultima actualizacion: 2026-06-23 (PWA sin mic + APK con voz, modelo relay, PND-099).