Salir de una sesión borró código que nunca habías commiteado


Ejecutas tres sesiones en paralelo. Una de ellas va por la mitad de editar un archivo: todavía sin commit, lo terminarás mañana. Sales de la sesión y cierras la terminal. A la mañana siguiente el directorio bajo .worktrees/ ha desaparecido, y la rama también.

No recordabas mal dónde estaba. A la limpieza de salida de worktree: true le falta una comprobación: mira los commits, nunca los archivos.

Qué hace el modo worktree

Arrancar una sesión con hermes -w (o poner worktree: true en la config) da a cada sesión del CLI su propio worktree bajo .worktrees/, en su propia rama:

worktree: false        # por defecto: solo cuando se pasa -w
worktree_sync: true    # por defecto: ramifica desde la punta remota recién fetcheada; false usa el HEAD local

Así es como varios agentes editan, commitean, pushean y abren PRs sin pisarse. Ramificar desde la punta remota fresca también es deliberado: el PR acaba acotado a tu cambio en lugar de heredar lo que el clon local llevara de retraso.

El problema está en el paso de salida.

Lo que promete la documentación frente a lo que hace el código

La documentación de configuración, en la sección de aislamiento con git worktree, es explícita:

Los worktrees limpios se eliminan al salir; los sucios se conservan para recuperación manual.

Mientras tanto, _cleanup_worktree en cli.py declara su propio criterio en el docstring:

def _cleanup_worktree(info: Dict[str, str] = None) -> None:
    """Elimina un worktree y su rama al salir; solo se conserva cuando tiene commits sin pushear."""

Solo se conservan los commits sin pushear. El helper que decide, _worktree_has_unpushed_commits(), ejecuta:

git log --oneline HEAD --not --remotes

En otras palabras, pregunta qué commits de HEAD no tiene ningún remoto. El estado del árbol de trabajo nunca se lee. Así que:

Qué contiene el worktree al salir Resultado real
Un archivo nuevo sin trackear, nunca commiteado worktree ELIMINADO
Un archivo trackeado modificado, sin commitear worktree ELIMINADO
Un commit aún sin pushear worktree conservado

El paso final es git worktree remove <path> --force más git branch -D <branch>. --force significa exactamente lo que parece: ignora los cambios sin commitear del árbol de trabajo.

Por qué hay dos criterios distintos

Hermes tiene en realidad dos rutas que gestionan worktrees, y no coinciden.

Una es la limpieza de salida en cli.py, la descrita arriba: solo commits.

La otra es el pruner de arranque en hermes_cli/worktree_ops.py, y esta sí entiende de «sucio»: _worktree_is_dirty() ejecuta git status --porcelain (incluidos los archivos sin trackear), falla en la dirección segura hacia True, y su veredicto se registra como dirty, que es un miembro de _PRESERVE_REASONS. El comentario que tiene encima es inusualmente directo:

En cada nivel y edad como defensa: los árboles sucios nunca se eliminan.

Así que no es que «Hermes no sepa distinguir sucio de limpio». Son dos rutas con dos criterios distintos: el pruner en segundo plano protege los archivos sucios, la limpieza de salida protege los commits. Los usuarios se topan con la ruta de salida.

Cómo protegerte

Hasta que se alinee ese criterio, esto es lo que funciona hoy.

  1. Commitea antes de salir, aunque sea como WIP. Un commit sin pushear hace que el árbol sobreviva:
    git add -A && git commit -m "wip: still working"
  2. O haz stash, incluyendo los archivos sin trackear. El -u de git stash -u es la parte importante: sin él, los archivos nuevos siguen existiendo solo dentro del worktree.
  3. No des por hecho que «la sesión sigue abierta» protege los archivos. El trabajo sin commitear solo está a salvo mientras vive la sesión; en el momento en que sales, entra en el radio de --force.
  4. Echa un vistazo al estado cuando ejecutes sesiones en paralelo. git -C .worktrees/<name> status --porcelain; una salida no vacía significa que este árbol se eliminará al salir.
  5. Haz que la sesión commitee y pushee. Las sesiones con worktree están diseñadas para que el agente commitee, pushee y abra PRs; una vez pusheado, el criterio de «commits sin pushear» conserva el árbol y ganas una segunda red de seguridad.
  6. Audita de vez en cuando. Merece la pena saber qué cubre y qué no la comprobación de worktree introducida en la v0.20.5.

Si ya te ha pasado: empieza por git worktree list y git branch para ver si la rama sobrevivió. Si había al menos un commit antes de salir, ese contenido sigue en el almacén de objetos y es recuperable desde la rama o el reflog. Pero los archivos sin commitear y sin trackear eliminados con --force no tienen ninguna vía de recuperación a nivel de git: que los recuperes depende de los buffers del editor, snapshots del sistema o copias de seguridad, no de git. Mejor ser directo con eso que insinuar un rescate que no existe.

En qué punto está esto

El issue #129482 se abrió el 2026-09-30, etiquetado como P1, contra el código en 41755854, y seguía abierto cuando se escribió esto. El arreglo natural es que la limpieza de salida llame también a _worktree_is_dirty(), de modo que ambas rutas compartan un único criterio. Hasta entonces, haz los pasos 1 y 2 de arriba.

Este tampoco es el primero de estos bugs de «parece limpio, pierde algo en silencio». La defensa de rollback para repos anidados cubre rollbacks que tocan directorios que nunca se capturaron, y los límites de sesión explica qué comparten realmente las sesiones. Merece la pena incorporar ambos a tus hábitos.

La conclusión

La lección no es «no uses worktrees». Es que una operación de borrado debería usar el criterio más conservador disponible: a la hora de decidir si un árbol puede eliminarse, lo que hay en el directorio de trabajo importa mucho más que lo que hay en el grafo de commits. Un único archivo sin commitear y a medio terminar suele valer más que diez commits pusheados.

Hasta que el código esté de acuerdo, la autodefensa cabe en una frase: commitea o haz stash antes de salir.