Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/verificacion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ jobs:
# Fuera de la lista, y no por olvido:
# verificar_router necesita Ollama con un modelo cargado.
# verificar_palabra_clave necesita la voz de Windows y un modelo de audio.
# verificar_recado necesita Windows (la bóveda cifra con DPAPI), Node y Chrome.
# El de la Fase D se queda: sin Ollama salta solas las comprobaciones que
# dependen del modelo y comprueba el resto.
- name: Verificadores
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ la web del móvil). No es una costumbre: lo comprueba `pruebas/test_arquitectura
| A qué agente va cada cosa | `perseo_core/infra/router.py` (el router) |
| Un agente concreto | `perseo_core/agentes/<nombre>.py` — se llaman como el agente |
| Qué necesita confirmación | `perseo_core/infra/politica.py` |
| Los recados en la web y la bóveda | `perseo_core/agentes/recado.py`, `servicios/boveda.py`, `servicios/navegacion.py` |
| Lo que Perseo hace solo: vigilar y retomar | `perseo_core/agentes/vigilancias.py`, `agentes/seguimiento.py`, `infra/disparadores.py` |
| El prompt compartido | `perseo_core/infra/identidad.py` |
| La llamada de voz | `RealTime/src/lib/llamada/gemini-live.ts` y `RealTime/src/App.tsx` |
| El panel | `RealTime/src/components/Panel.tsx` |
Expand Down
28 changes: 20 additions & 8 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ see [ADR 0005](docs/adr/0005-las-confirmaciones-estan-apagadas.md).
[![MIT licence](https://img.shields.io/badge/licence-MIT-black.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-black.svg)](https://www.python.org/)
[![Tauri 2](https://img.shields.io/badge/tauri-2-black.svg)](https://tauri.app/)
[![919 tests](https://img.shields.io/badge/tests-919-black.svg)](#verification)
[![1230 tests](https://img.shields.io/badge/tests-1230-black.svg)](#verification)

[What it is](#what-it-is) · [What it looks like](#what-it-looks-like) ·
[How it works](#how-it-works) · [Install](#install) · [Privacy](#privacy) ·
Expand Down Expand Up @@ -48,9 +48,12 @@ Raspberry Pi tomorrow without rewriting a line of interface code.
| 📬 **Mail triaged before you read it** | Every message lands in a bucket — ignore, interesting, needs action, not sure — decided by a **local** model, on your GPU |
| 🧠 **Real memory** | Markdown notes in your Obsidian vault. It searches, reads and **appends**; never overwrites, never deletes |
| 👤 **It knows who's talking** | Recognises voices and faces with local models, and learns people it hasn't met. Off by default |
| 🛑 **It grades risk, and knows who asked** | Four levels enforced in the worker, not in the prompt. Switched on, anything irreversible stops and waits for your yes — and an order from a guest stops even while you are right there. **They ship off** as of 2026-09-12: [ADR 0005](docs/adr/0005-las-confirmaciones-estan-apagadas.md) |
| 🛑 **It grades risk, and knows who asked** | Five levels enforced in the worker, not in the prompt. Switched on, anything irreversible stops and waits for your yes — and an order from a guest stops even while you are right there. **They ship off** as of 2026-09-12 ([ADR 0005](docs/adr/0005-las-confirmaciones-estan-apagadas.md)), except whatever leaves the house ([ADR 0007](docs/adr/0007-lo-que-sale-de-casa-se-para.md)) |
| 📱 **It follows you to your phone** | A PWA over your home VPN: chat, queue, mail and status. No build step, one single file |
| 🤖 **It delegates code** | Hands tasks to sub-agents (Claude Code or opencode) and tells you how they're going while they work |
| ✉️ **It sends and schedules** | Sends the email it drafted for you and puts appointments in your Google Calendar. Whatever reaches another person —the email, the invitation— waits for your yes on the card, and the email only goes out if it is exactly the draft you approved |
| 👁️ **It watches and follows up** | "Tell me when tickets go on sale" —or "book it as soon as the price drops"— and it checks every few hours until it happens. And if an email that asked for something has had no reply from you in Gmail for two days, it reminds you once |
| 🧾 **It runs errands on the web** | "Book me a table on Friday" and it just does it, with its own Chrome and your sessions, and calls you when it's done. Passwords come from an encrypted vault the model never sees, and paying or booking waits for your yes: [ADR 0007](docs/adr/0007-lo-que-sale-de-casa-se-para.md) |
| 🔌 **It speaks MCP** | Its own client for local and remote servers: vault, browser, Windows, triaged mail, sub-agents |

---
Expand Down Expand Up @@ -171,11 +174,16 @@ implementation behind it today, another tomorrow, without touching the agent.
|---|---|---|
| `correo` (mail) | Triages incoming: ignore / interesting / needs action / not sure | Gmail, or a JSON file |
| `agenda` (calendar) | Warns about what starts soon, once per event | Google Calendar, or a JSON file |
| `recordatorios` (reminders) | “Remind me in twenty minutes”, “remind me on Thursday at five”: stores, repeats and fires them — late, and saying so, if the core was down | A JSON file in `<datos>/` |
| `parte` (daily brief) | The day at a glance: calendar, emails that need something, reminders, tasks and habits. Optionally every morning | What the other agents already read |
| `memoria` (memory) | Searches, reads and **appends** to the vault. Never overwrites or deletes | Markdown files, or Obsidian's REST plugin |
| `chat` | Runs the written chat in the panel and on the phone, using the other agents' tools | Gemini REST with function calling |
| `dev` | Hands coding tasks to a sub-agent and **reports progress** while it works | `claude-agent-sdk`, or `claude -p`, or opencode |
| `pc` | Opens apps, types, moves the mouse. Allow-list, no shell | `pyautogui` (optional) |
| `web` | Reads pages and searches. Can't reach your home network | HTTP, no browser |
| `recado` | Runs a whole errand on the web in the background: logs in with your vault passwords, stops before paying or booking, and picks up from there with your yes | Chrome through `@playwright/mcp` and Gemini; in tests, a script and fake pages |
| `vigilancias` | Keeps the list of things to watch; a trigger launches a `recado` every few hours to check whether it has happened yet, and stays quiet while it hasn't | `<datos>/vigilancias.json` |
| `seguimiento` | Reminds you once about emails that asked for something and got no reply in two days; whatever you already answered in Gmail it marks as handled | The Gmail thread, only its labels |
| `mcp` | Talks to the declared MCP servers: vault, browser, Windows, time, triaged mail | JSON-RPC over stdio with its own client; remote ones over HTTP with the official SDK |
| `eco`, `simulacro` | The two test agents: echo back what they get, touch nothing | — |

Expand Down Expand Up @@ -211,11 +219,15 @@ three levels enforced **in the worker**, before the agent runs:
| `irreversible` | Typing blind — and **anything not classified** | Stops and asks for a yes |

There is a fourth one, `critico` — deleting, touching the registry, killing
processes — that **always** asks, trust mode or not.
processes — that **always** asks, trust mode or not. And a fifth, `exterior` —
whatever reaches another person or spends money: paying, booking, sending,
entering a card — that always asks **and whose yes the model cannot give**,
typed or spoken: it's given on the card in the panel or on the phone.

> **And one switch above all of it.** As of 2026-09-12
> `politica.CONFIRMACIONES` is `False`, and the right-hand column reads
> "runs" on all four rows: nothing stops, `critico` included. The table, the
> "runs" on every row but one: nothing stops, `critico` included, except
> `exterior` ([ADR 0007](docs/adr/0007-lo-que-sale-de-casa-se-para.md)). The table, the
> levels and their tests are all still there — the only thing that never
> happens is the stop. Why, what it costs and how to re-arm it — one line, or
> `PERSEO_CONFIRMACIONES=1` — are in [ADR 0005](docs/adr/0005-las-confirmaciones-estan-apagadas.md).
Expand Down Expand Up @@ -376,13 +388,13 @@ the tables read fine in any language.
None of this is checked by eye, and it's checked two ways.

**Unit tests** — each piece on its own, no network, no subprocesses. They tell
you *what* broke: **919** in total.
you *what* broke: **1230** in total.

```bash
python commands/perseo.py comprobar # everything, cheapest first

python -m pytest # 760, core and commands
cd RealTime && npm test # 159, the interface
python -m pytest # 1026, core and commands
cd RealTime && npm test # 204, the interface
cd RealTime/src-tauri && cargo check # and that the Rust compiles
```

Expand Down Expand Up @@ -433,7 +445,7 @@ details are in [`docs/PRIVACIDAD.md`](docs/PRIVACIDAD.md).

Perseo works and gets used daily, but it's a personal project: built for
**one** person on **one** Windows machine, and it shows. Behind it are roughly
49,100 lines, 919 tests and 17 verifiers.
49,100 lines, 1230 tests and 19 verifiers.

If you clone it and something won't start, open an
[issue](https://github.com/PersusUS/Perseo/issues) — and if you fix it, even
Expand Down
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ irreversible a esperar tu sí. Esa parada viene apagada: [ADR 0005](docs/adr/000
[![Licencia MIT](https://img.shields.io/badge/licencia-MIT-black.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-black.svg)](https://www.python.org/)
[![Tauri 2](https://img.shields.io/badge/tauri-2-black.svg)](https://tauri.app/)
[![919 pruebas](https://img.shields.io/badge/pruebas-919-black.svg)](#verificación)
[![1230 pruebas](https://img.shields.io/badge/pruebas-1230-black.svg)](#verificación)

[Qué es](#qué-es) · [Cómo se ve](#cómo-se-ve) · [Cómo funciona](#cómo-funciona) ·
[Instalación](#instalación) · [Privacidad](#privacidad) · [English](README.en.md)
Expand All @@ -42,9 +42,12 @@ Raspberry Pi sin reescribir una línea de la interfaz.
| 📬 **Correo triado antes de que lo leas** | Cada mensaje cae en un cajón —ignorar, interesante, requiere acción, no seguro— decidido por un modelo **local**, en tu GPU |
| 🧠 **Memoria de verdad** | Notas Markdown en tu vault de Obsidian. Busca, lee y **añade**; nunca sobrescribe ni borra |
| 👤 **Sabe quién habla** | Reconoce voces y caras con modelos locales, y aprende solo a quien no conoce. Apagado de fábrica |
| 🛑 **Clasifica por riesgo, y sabe quién lo pide** | Cuatro niveles aplicados en el trabajador, no en el prompt. Encendidos, lo irreversible se para y espera tu sí — y una orden de una visita se para aunque estés delante. **Vienen apagados** desde el 2026-09-12: [ADR 0005](docs/adr/0005-las-confirmaciones-estan-apagadas.md) |
| 🛑 **Clasifica por riesgo, y sabe quién lo pide** | Cinco niveles aplicados en el trabajador, no en el prompt. Encendidos, lo irreversible se para y espera tu sí — y una orden de una visita se para aunque estés delante. **Vienen apagados** desde el 2026-09-12 ([ADR 0005](docs/adr/0005-las-confirmaciones-estan-apagadas.md)), salvo lo que sale de casa ([ADR 0007](docs/adr/0007-lo-que-sale-de-casa-se-para.md)) |
| 📱 **Te sigue al móvil** | Una PWA por la VPN de casa: chat, cola, correo y estado. Sin build y en un solo fichero |
| 🤖 **Delega código** | Encarga tareas a subagentes (Claude Code u opencode) y te cuenta por dónde van mientras trabajan |
| 🧾 **Hace recados en la web** | «Resérvame mesa el viernes» y lo hace solo, con su propio Chrome y tus sesiones, y te llama al acabar. Las claves salen de una bóveda cifrada que el modelo nunca ve, y pagar o reservar esperan tu sí: [ADR 0007](docs/adr/0007-lo-que-sale-de-casa-se-para.md) |
| ✉️ **Envía y apunta** | Manda el correo que te redactó y apunta citas en tu Google Calendar. Lo que llega a otra persona —el correo, la invitación— espera tu sí en la tarjeta, y el correo solo sale si es exactamente el borrador que aprobaste |
| 👁️ **Vigila y retoma** | «Avísame cuando haya entradas» —o «resérvalo en cuanto baje»— y mira cada pocas horas hasta que pasa. Y si un correo que pedía algo lleva dos días sin respuesta tuya en Gmail, te lo recuerda una vez |
| 🔌 **Habla MCP** | Cliente propio para servidores locales y remotos: vault, navegador, Windows, correo triado, subagentes |

---
Expand Down Expand Up @@ -165,11 +168,16 @@ una implementación y mañana otra, sin tocar el agente.
|---|---|---|
| `correo` | Tría el entrante: ignorar / interesante / requiere acción / no seguro | Gmail, o un fichero JSON |
| `agenda` | Avisa de lo que empieza pronto, una vez por evento | Google Calendar, o un fichero JSON |
| `recordatorios` | «Avísame en veinte minutos», «recuérdame el jueves a las cinco»: apunta, repite y avisa, también tarde si el núcleo estaba apagado | Un fichero JSON en `<datos>/` |
| `parte` | El parte del día de una vez: agenda, correos que piden algo, recordatorios, tareas y hábitos. Opcionalmente, cada mañana | Lo que ya leen los demás |
| `memoria` | Busca, lee y **añade** en el vault. No sobrescribe ni borra | Ficheros Markdown, o el plugin REST de Obsidian |
| `chat` | Sostiene el chat escrito del panel y del móvil, y usa las herramientas de los demás | Gemini REST con *function calling* |
| `dev` | Encarga tareas de código a un subagente y **cuenta por dónde va** mientras trabaja | `claude-agent-sdk`, o `claude -p`, o opencode |
| `pc` | Abre apps, teclea, mueve el ratón. Lista blanca y sin shell | `pyautogui` (opcional) |
| `web` | Lee páginas y busca. No alcanza la red de casa | HTTP, sin navegador |
| `recado` | Hace un encargo entero en la web, en segundo plano: entra con tus claves de la bóveda, se para antes de pagar o reservar y sigue desde ahí con tu sí | Chrome por `@playwright/mcp` y Gemini; en las pruebas, un guion y páginas de mentira |
| `vigilancias` | Apunta lo que hay que vigilar; un disparador lanza cada pocas horas un `recado` que mira si ya se cumple, y calla mientras no | `<datos>/vigilancias.json` |
| `seguimiento` | Avisa una vez de los correos que pedían algo y llevan dos días sin respuesta; lo que ya contestaste en Gmail lo da por atendido | El hilo de Gmail, solo sus etiquetas |
| `mcp` | Habla con los servidores MCP declarados: vault, navegador, Windows, tiempo, correo triado | JSON-RPC por stdio con cliente propio; los remotos, por HTTP con el SDK oficial |
| `eco`, `simulacro` | Los dos de pruebas: devuelven lo que reciben, sin tocar nada | — |

Expand Down Expand Up @@ -206,11 +214,15 @@ agente se ejecute:
| `irreversible` | Teclear a ciegas, y **todo lo que no esté clasificado** | Se para y pide un sí |

Y hay un cuarto, `critico` —borrar, tocar el registro, matar procesos—, que
pregunta **siempre**, con modo confianza o sin él.
pregunta **siempre**, con modo confianza o sin él. Y un quinto, `exterior`
—lo que llega a otra persona o gasta dinero: pagar, reservar, enviar, meter una
tarjeta—, que pregunta siempre **y cuyo sí no puede darlo el modelo**, ni
escribiendo ni hablando: se da en la tarjeta del panel o del móvil.

> **Y un interruptor por encima de todo esto.** Desde el 2026-09-12,
> `politica.CONFIRMACIONES` está en `False` y la columna de la derecha dice
> «se ejecuta» en las cuatro filas: nada se para, tampoco lo `critico`. La
> «se ejecuta» en todas las filas menos una: nada se para, tampoco lo `critico`,
> salvo lo `exterior` ([ADR 0007](docs/adr/0007-lo-que-sale-de-casa-se-para.md)). La
> tabla, los niveles y sus pruebas siguen enteros — lo único que no llega a
> aplicarse es la parada. El porqué, lo que cuesta y cómo se rearma —una
> línea, o `PERSEO_CONFIRMACIONES=1`— están en [ADR 0005](docs/adr/0005-las-confirmaciones-estan-apagadas.md).
Expand Down Expand Up @@ -369,13 +381,13 @@ y las rutas de la API en [`docs/API.md`](docs/API.md).
Nada de esto se comprueba a ojo, y se comprueba de dos maneras.

**Pruebas unitarias** — cada pieza por separado, sin red y sin subprocesos.
Dicen *qué* se ha roto: **919** en total.
Dicen *qué* se ha roto: **1230** en total.

```bash
python commands/perseo.py comprobar # todo, en orden de coste

python -m pytest # 760, el núcleo y los comandos
cd RealTime && npm test # 159, la interfaz
python -m pytest # 1026, el núcleo y los comandos
cd RealTime && npm test # 204, la interfaz
cd RealTime/src-tauri && cargo check # y que el Rust compila
```

Expand Down Expand Up @@ -437,7 +449,7 @@ El detalle, en [`docs/PRIVACIDAD.md`](docs/PRIVACIDAD.md).

Perseo funciona y se usa a diario, pero es un proyecto personal: está pensado
para **una** persona, en **un** ordenador con Windows, y se nota. Lo que hay
detrás son unas 49.100 líneas, 919 pruebas y 17 verificadores.
detrás son unas 49.100 líneas, 1230 pruebas y 19 verificadores.

Si lo clonas y algo no arranca, abre un
[issue](https://github.com/PersusUS/Perseo/issues) — y si lo arreglas, mejor
Expand Down
43 changes: 43 additions & 0 deletions RealTime/pruebas/autollamada.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Qué se hace con un aviso de llamada. Ver `lib/llamada/autollamada.ts`.
*
* El fallo que esto arregla: un encargo que terminaba en plena llamada se
* tiraba, porque la regla «ya está en llamada, no hagas nada» era para la
* palabra clave y se aplicaba también a los avisos con motivo.
*/
import { describe, expect, it } from 'vitest';

import { avisoEnLlamada, decidirAviso } from '../src/lib/llamada/autollamada';

describe('decidirAviso', () => {
it('un encargo o un recordatorio en plena llamada se cuenta en vivo', () => {
expect(decidirAviso('Recordatorio: el té', 'connected', false)).toBe('contar-en-vivo');
});

it('la palabra clave en plena llamada no corta nada', () => {
expect(decidirAviso('', 'connected', false)).toBe('nada');
});

it('mientras conecta, se guarda para contarlo al abrir', () => {
expect(decidirAviso('El encargo #3 ha terminado.', 'connecting', false)).toBe('guardar');
});

it('sin llamada, un motivo suena el timbre y decide él', () => {
expect(decidirAviso('Recordatorio: el té', 'disconnected', false)).toBe('timbre');
expect(decidirAviso('Recordatorio: el té', 'error', true)).toBe('timbre');
});

it('sin motivo es la palabra clave: entra en llamada, salvo recién abierta', () => {
expect(decidirAviso('', 'disconnected', false)).toBe('llamar');
expect(decidirAviso('', 'disconnected', true)).toBe('nada');
});
});

describe('avisoEnLlamada', () => {
it('cada motivo es una línea, y va marcado como aviso del sistema', () => {
const texto = avisoEnLlamada('Recordatorio: el té\nEl encargo #3 ha terminado.\n');
expect(texto.startsWith('[AVISO DEL SISTEMA')).toBe(true);
expect(texto).toContain('- Recordatorio: el té');
expect(texto).toContain('- El encargo #3 ha terminado.');
});
});
Loading
Loading