携帯 Yujin Forge Mobile

Funcionalidad total -- mobile companion

Dos clientes: una PWA (web app, sin tienda, sin microfono) y una app nativa de Android (APK) que agrega voz on-device. El telefono es un control remoto del escritorio via el relay HITO 4.
Manual operativo, no marketing. Ultima actualizacion: 2026-06-23.

Tabla de contenidos

  1. Qué es Forge Mobile en una frase
  2. Arquitectura cliente / servidor
  3. Shell de UI -- 3 modos
  4. Chat send + branch por pair state
  5. Como el telefono obtiene respuestas (relay)
  6. Voz -- solo en la APK nativa
  7. LAN discovery (mDNS + polling)
  8. State channel cross-device
  9. Pairing QR + 6 dígitos
  10. Storage adapter (3 scopes)
  11. Transcript persistence
  12. SSE client RN-compatible
  13. BYOK keys (locales en el escritorio)
  14. SettingsPanel (5 cards)
  15. NAC-3 attrs + a11y verbs
  16. Permisos + metadata (app.json)
  17. i18n 10 idiomas
  18. Lo que mobile NO incluye
  19. Estado del install + EAS profiles
  20. 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:

CapaDonde vivePropósito
Appapps/forge-mobile/Expo binding del shell + adapters + assets sumi-e.
Cross-platform packagepackages/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 4Cloudflare 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 ForgeTu máquina, yf chatAnuncia _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).

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):

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:

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.

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.

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:

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.

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:

  1. CLI genera un código de 6 dígitos (1e6 entropy, fácil leer en voz).
  2. 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).
  3. CLI emite el blob encriptado + el salt + el hash del código como JSON QR.
  4. 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:

ScopeBackendQué guarda
secretexpo-secure-store (device keychain)HMAC key del HITO 4, user handle, JWT/PAT. Acceso bloqueado por jailbreak / root.
settingIn-memory hoy (AsyncStorage swap es 1 línea)Provider preference, language, voice mode, STT/TTS picks.
sessionIn-memoryEstado 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:

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:

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.

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:

CardContenidoMutators
Plan herostatus (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
Voice3 toggles: modo (mic/manual), STT (browser/google/whisper), TTS (browser/google/elevenlabs).onSetVoiceMode, onSetStt, onSetTts
Language10 chips: es / en / pt / fr / de / it / ja / zh / hi / ar.onSetLanguage
AboutForge 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:

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)

PlataformaPermiso / configRazón
iOSNSMicrophoneUsageDescriptionVoice loop (record + transcribir).
iOSNSLocalNetworkUsageDescriptionLAN discovery (mDNS browse).
iOSNSBonjourServices: ["_yujin-forge._tcp"]Entitlement para anunciar / leer el servicio mDNS.
iOSsupportsTablet: trueiPad-friendly.
AndroidRECORD_AUDIOVoice loop.
AndroidINTERNETHTTPS al worker HITO 4.
AndroidACCESS_NETWORK_STATEDetectar online/offline + Wi-Fi.
AndroidCHANGE_WIFI_MULTICAST_STATEmDNS browse (sin esto el chip no lee multicast).
Ambosscheme: "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.

Lo que mobile NO incluye

Decisiones de diseño, no olvidos. Mantienen mobile delgado y fuerzan laptop-first para tareas sensitive:

FeaturePor qué no en mobile
Vault management completoMobile solo expone los slots necesarios para chat + voz. Slot catalog + import-legacy + redactor viven laptop-only.
Limits config editorLas limits viven en el laptop (~/.yujin-forge/limits.json); mobile las hereda por state channel.
Audit log previewLa jsonl del audit log vive en el laptop; mobile no tiene acceso al disco del laptop.
Danger ZoneIntencionalmente laptop-only por seguridad. El device más liviano nunca puede auto-aprobar destructivos.
VAD tuning wizardEn 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 licenciaMobile asume que el pairing ya está hecho desde el laptop -- ahí se eligió Plan vs Console BYOK.
Pending registry CLIMobile no edita pendings -- es herramienta de desarrollo del producto.
Brain matrix QA harnessMobile 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):

OK
App buildable + typecheck verde
Expo 51 / RN 0.74.5 / React 18.2. pnpm --filter @yujin/forge-mobile typecheck pasa en HEAD.
alpha.59z.179
OK
APK por GitHub Actions (gratis) -> release mobile-latest
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.
live
OK
PWA desplegada por el worker
expo export -p web -> apps/forge-mobile/dist, servido por el worker HITO 4 via [assets]. Instalable desde el navegador del telefono.
live
Google Play internal track
Requires Apple Dev / Google Play accounts. Submit profile production scaffold listo con placeholders TODO en eas.json; .gitignore cubre el service account JSON.
Phase 1 -- USD 25
iOS TestFlight
Requires Apple Developer account. Bonjour entitlement on-device validation requires un TestFlight build instalado.
Phase 2 -- USD 99/yr
Expo OTA updates
Push de bundles JS sin re-submit a las stores. Requiere builds de Phase 1 o 2 antes.
Phase 3
Drop documents desde el phone
El desktop tiene 10-format ingest; mobile todavía no. Backlog post-Phase 1.
backlog
Queue offline
Hoy mobile degrada gracefully sin LAN; no hay sync queue. Backlog post-Phase 1.
backlog

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).

ProfileDistributionChannelAndroid formatUso
internalinternalinternalAPKSideload testing en device físico.
previewinternalpreviewAPKCandidates antes de promover a TestFlight / Play internal.
productioninternalproductionAPKDirect-install legacy (apk sideload).
production-storestoreproductionAAB (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):

ArchivoCasosCubre
tests/mobile-transcript-store.test.ts12read / append / clear / listProjects / cap eviction / corruption filter / serialised writes.
tests/mobile-sse-client.test.ts11parseSseChunks 8 casos + connectSse loop 3 casos (chunks, HTTP error, close()).
tests/mobile-managed-client.test.ts5happy path, tool roundtrip, auth-missing error, 5xx server error, maxToolRounds bound.
tests/nac3-rn-a11y-adapter.test.ts13announce / focus / label + slug registry + label override broadcaster (PND-009).
tests/mobile-send-classifier.test.tsBranch 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).