Land vs ship
Mergeá una mission localmente, o enviala por el pipeline tribunal a un pull request real.
Una mission terminada sale de su camp de dos maneras. Las dos son explícitas, y ninguna es silenciosa.
vx land | vx ship | |
|---|---|---|
| Resultado | fast-forward local de tu checkout | un pull request real de GitHub |
| Pipeline de revisión | ninguno; la revisaste vos | lint, tests, una revisión adversarial, un chequeo de docs (lint y tests requieren un proyecto Go o Node, docs solo Go) |
| Necesita | un checkout limpio | gh instalado |
| Sale de la máquina | no | sí, hace push a origin |
Por defecto el commander pregunta antes de aterrizar. Con yolo mode activado en el proyecto, aterriza sin preguntar una mission terminada y verificada. Yolo nunca cubre vx ship.
A shipped soldier waits on purpose
Después de un vx ship exitoso, el pane y el camp del soldier quedan abiertos hasta que mergees el pull request. Es a propósito, no es un soldier trabado. Ver Por qué un soldier shipped sigue abierto más abajo.
Land
vx land <task-id>Hace fast-forward del checkout de tu proyecto hasta la branch de la mission, y después hace strike del camp y cierra su pane. Se niega, dejando todo intacto, si tu checkout está sucio o la branch divergió de la base. Nunca fuerza ni hace rebase.
Ship
vx ship <task-id>Corre, en orden, dentro del camp de la mission, y se detiene en el primer fallo sin hacer push de nada:
- lint -
go vet ./...en un proyecto Go,npm run lint --if-presenten uno Node; - tests -
go test ./...onpm run test --if-present; - review - una revisión adversarial del diff hecha por una sesión nueva del agente en el que corrió la mission (ver abajo);
- docs - en proyectos Go, las declaraciones exportadas nuevas deben llevar un comentario de documentación. En cualquier otro proyecto este paso se saltea y figura como aprobado.
La revisión adversarial
La revisión corre en una sesión headless nueva del harness en el que corrió la mission, Claude Code por defecto (nunca una reanudada), que no tiene nada del contexto del autor. Recibe la branch, los commits base y objetivo y la lista de archivos cambiados que calculó vexillum - no el diff. Lee el diff ella misma con git, asume que el cambio está mal e intenta romperlo: correctness, casos borde, manejo de errores, concurrencia, seguridad y autorización, regresiones y la calidad de los tests nuevos. Un hallazgo necesita un escenario de fallo concreto del uso previsto del cambio, y cada clase de defecto se reporta una sola vez con todos sus sitios hermanos. En Claude Code corre con sonnet en esfuerzo high; en Pi y OpenCode corre con el modelo y el nivel de thinking con los que corrió la mission, o el default del harness cuando el perfil no definió ninguno. No corre los tests (ya los corrió el paso anterior).
Cuando la mission tiene prompt, cada componente que el cambio introdujo también se juzga contra ese enunciado más las instrucciones que el general dio después, con vx prompt y vx decide. Esas quedan registradas en la tarea como amendments (con fecha y el comando de origen, en vx status --json) y se listan después del prompt, en orden, así que un componente que pediste más tarde no se reporta como no pedido. Solo esos dos comandos escriben amendments, en el archivo de estado de la tarea y nunca en un archivo del camp, así que un soldier no puede ampliar su propio mandato. Lo que no pidió ni el prompt ni un amendment se reporta como warning con remedio "quitarlo". Una mission sin prompt omite esta pasada, y ship lo dice.
El revisor devuelve JSON: hallazgos (file, line, severity error/warning/info, action ask-user/auto-fix/no-op, descripción, escenario de fallo, sitios hermanos), los reviewed_paths que realmente leyó y un nivel de riesgo, más el pr_title y el pr_description opcionales que pasan a ser el texto del pull request (los inválidos nunca hacen fallar la revisión, ver abajo). Si el JSON no valida, la revisión se reintenta (hasta dos veces más). Después:
- cualquier hallazgo de severidad error o warning bloquea el ship, incluidos los
ask-user, que necesitan tu decisión; los info no bloquean; - un archivo cambiado que falta en
reviewed_pathscuenta como no revisado y hace fallar el paso; - sin JSON válido tras los reintentos, o un timeout, el paso falla. Ninguno de los dos es jamás un pass.
Los hallazgos se imprimen en la salida de vx ship. Si el ship pasa, los hallazgos info se agregan al cuerpo del pull request bajo Tribunal notes.
Revisar, arreglar, revisar (opt-in)
vx ship <task-id> --fix [--max-rounds 2] [--timeout 20m]Sin --fix, una revisión que bloquea solo rechaza el ship e imprime los hallazgos. Con --fix, los hallazgos marcados auto-fix van a un fixer headless en el camp, que arregla la invariante en todos los sitios hermanos (y prefiere quitar un componente no requerido a endurecerlo). vexillum commitea su trabajo, corre lint y tests de nuevo, y un revisor nuevo vuelve a revisar, sabiendo que los commits del fixer son código sin revisar y deben cumplir el mismo estándar. Quien prescribió una corrección jamás la certifica.
Los hallazgos ask-user nunca se auto-corrigen: si algún hallazgo bloqueante necesita a un humano, ship rechaza sin ronda de arreglo. --max-rounds limita las rondas de arreglo (por defecto 2); agotarlas sin una revisión que pase rechaza el ship. --timeout limita cada corrida de revisor o fixer (por defecto 20 minutos).
Sin go.mod ni package.json, lint y tests pasan con una nota de que no se detectó nada: no se corrió nada, así que ese pase no dice nada sobre el código. Un proyecto Node sin script lint o test pasa igual, por el --if-present. Es sincrónico: cuando ship vuelve, todo el pipeline terminó. Solo después de que pasan todos los pasos hace push de la branch del camp a origin y corre gh pr create. Ningún agente decide si eso ocurre ni cuándo: es la única acción con un efecto irreversible fuera de la máquina.
Una mission en done o shipped puede enviarse de nuevo para empujar seguimientos al mismo PR; vuelve a correr el pipeline completo primero. Siempre que ya exista un pull request para la branch, vx ship lo reutiliza en vez de abrir un segundo: hace push a la misma branch y refresca el título y la descripción del pull request con la nueva revisión (ver más abajo). Un pull request que ya está mergeado o cerrado no puede recibir más commits, así que vx ship se niega antes de correr nada.
Una branch rebaseada
Si el soldier hizo rebase o amend de una branch que vx ship ya había pusheado, el siguiente push se rechaza como non-fast-forward. vx ship registra en la tarea el tip de cada push que hace (last_pushed_sha) y, ante ese rechazo, lee el tip que tiene origin para la branch. Cuando ese tip es exactamente el registrado, la reescritura es obra de vexillum, así que reintenta una vez con un lease atado a ese commit:
git push --force-with-lease=<branch>:<sha registrado> origin <branch>El lease nombra solo la branch de la mission, nunca la branch base ni ninguna otra ref, y git lo rechaza si origin se movió mientras tanto. vx ship imprime una línea (rewrote the PR branch <branch> on origin (force-with-lease) from <old> to <new>), refresca el título y la descripción del pull request e imprime su URL.
Cuando el tip de origin es cualquier otro, porque alguien más pusheó a la branch, o cuando no hay ningún push registrado, vx ship nunca fuerza. Falla y dice qué hay en origin y cómo decidir: corré git fetch origin <branch> en el camp e inspeccionalo, o autorizá un push con lease manual con el SHA de origin que imprimió, y después corré vx ship <task-id> de nuevo.
El texto del pull request
El pull request es público y su texto no requiere ningún paso manual. La revisión adversarial lee el diff completo y los commits sin contexto del autor, y además devuelve un pr_title en estilo conventional-commit (unos 72 caracteres) y un pr_description breve y factual (qué cambió y por qué, de 3 a 8 líneas de markdown plano), escritos a partir del diff y nunca del prompt de la mission. vx ship los usa como título y como sección What. El resto del cuerpo se arma con hechos: una línea Changes (archivos cambiados, inserciones, borrados y las áreas de primer nivel tocadas), una línea Verification (los pasos del tribunal que pasaron, las rondas de revisión y las de arreglo), los hallazgos info de la revisión bajo Tribunal notes, y un pie de una línea que nombra la mission.
Cuando el revisor omite esos campos o devuelve unos inválidos (un título que no es de una línea en estilo conventional-commit, una descripción con encabezados, o cualquier texto que no pase el saneado), vx ship lo informa y recurre, campo por campo, a texto derivado de la branch: el título es el subject del único commit de la branch, o de su commit feat, fix o docs más reciente, o de su primer commit, cortado a unos 72 caracteres, y What es la lista de subjects de los commits.
Todo lo que viene del camp o del revisor se sanea antes de publicar: se descartan las líneas con una ruta absoluta de home (/Users/..., /home/...), un puerto de localhost o algo que parezca un secreto, y también cualquier línea que cite el prompt de la mission. El texto del revisor que no pasa la comprobación se reemplaza por el texto derivado en lugar de publicarse recortado, y vx ship informa cuántas líneas descartó de lo demás.
Los overrides son opcionales:
vx ship <task-id> --title "feat: titulo conciso" --body-file description.md--title reemplaza el título. --body "<texto>" o --body-file <ruta> (- lee la entrada estándar; no se pueden combinar) reemplaza la sección What; Changes, Verification, Tribunal notes y el pie se mantienen. Un --title que no pasa la comprobación de seguridad se rechaza antes de que corra el tribunal. Cuando el pull request ya existe, los overrides se aplican al refresco; sin ellos el texto refrescado sale de la nueva revisión, así que un texto editado a mano en GitHub se reemplaza.
Aterrizar una mission enviada
vx land <task-id>Para una tarea enviada, land mergea el pull request real. Requiere gh y se niega salvo que el PR esté abierto, no sea draft, sea mergeable y todos los checks estén en verde; el merge queda atado al head exacto que acaba de verificar. No hace strike del camp: la branch sigue en vuelo hasta que el PR realmente se mergee.
Por qué un soldier shipped sigue abierto
Después de un vx ship exitoso el status de la mission es shipped: la branch está pusheada y hay un pull request real abierto, pero el trabajo no está aterrizado localmente, así que vx ship no hace strike del camp. El pane del soldier y su camp (worktree) quedan abiertos a propósito hasta que el pull request se mergee. No está trabado ni varado: aparece como shipped en vx status, y en Shipped del tablero de muster.
El motivo es práctico. Si el CI, un reviewer o vos piden cambios en el pull request, el mismo soldier todavía tiene todo el contexto y la branch montada. vx prompt acepta una tarea shipped como cualquier otra terminada:
vx prompt <task-id> "atendé los comentarios de la revisión"El texto va al pane del soldier y queda registrado como amendment, como cualquier otro vx prompt. La tarea pasa a running mientras el soldier trabaja, y cuando se asienta vuelve a done, no a shipped, porque los commits nuevos todavía no están en el pull request. El commander recibe un aviso por ese asentamiento. Después corré vx ship <task-id> de nuevo: corre el pipeline completo, los commits se pushean a la misma branch y, como ya existe un pull request para la branch, se reutiliza y se refresca su título y descripción, nunca se abre un segundo.
Cómo termina
Mergeá el pull request, sea en GitHub o con vx land <task-id>. Después, desde el checkout de tu proyecto, en la branch base:
git pull
vx strike <task-id>vx strike nunca hace fetch. Compara el camp solo contra tu branch base local: el worktree no debe tener cambios sin commitear y los commits del camp ya deben estar en esa branch. Justo después de un merge en GitHub, y antes del pull, tu base local todavía no los tiene, así que vx strike se niega y deja el camp y el pane intactos. El rechazo dice qué hacer: mergear el pull request, correr git pull en la branch base y reintentar. Después de git pull funciona, también con un merge squash o rebase: además del chequeo de ancestro simple, acepta un camp cuyo contenido la base ya tiene bajo otros SHAs de commit. Entonces devuelve el camp al pool y cierra el pane.
Cuando GitHub dice que mergeó
Para una tarea shipped, y solo para esa, vx strike también le pregunta a gh si el pull request de la branch del camp está MERGED. Es de mejor esfuerzo y nunca obligatorio: necesita gh instalado y con sesión iniciada, y sin eso strike se apoya solo en los chequeos locales. Una tarea que no está shipped no hace ninguna llamada de red.
Un pull request mergeado cuenta como aterrizado una vez que su commit de merge es alcanzable desde tu branch base local, y se confía en la palabra de GitHub antes que en el chequeo de contenido. Importa porque el chequeo de contenido puede negarse para siempre después de un merge remoto cuando commits posteriores en la base tocaron las mismas líneas. Si el commit de merge todavía no está en la base local, strike te dice que hagas git pull. Un camp con commits que el pull request mergeado nunca llevó se sigue rechazando.
Limpieza de la branch después de un strike
Tras un strike exitoso, vx strike también poda. Borra la branch local de la tarea vexillum/<task-id> con la semántica de git branch -d, nunca -D, cuando la branch es ancestro de la branch base. También cuenta una tarea shipped cuyo pull request gh reporta como mergeado, con el commit de merge en tu base local, aunque un merge squash o rebase no sea ancestro. Nunca borra una branch que está checked out en otro worktree. Cuando nada de eso se cumple, la branch se conserva y una línea dice por qué y qué podés hacer, por ejemplo git branch -D cuando estés seguro. Además corre git worktree prune en el repo del proyecto para que no se acumulen registros de camps viejos, e imprime lo podado en una línea corta. --discard sigue las mismas reglas, así que una branch sin aterrizar sobrevive.
Hacer strike igual con --discard
vx strike <task-id> --discard--discard hace strike del camp aunque ese chequeo falle o el worktree tenga cambios sin commitear, y tira ese trabajo. Imprime exactamente qué descartó: cada commit sin aterrizar (hash y asunto) y cada cambio sin commitear, y resetea el worktree para que el slot pueda reutilizarse. Los commits siguen alcanzables desde la branch del camp hasta que el slot se reutilice.
Es para un solo caso: el trabajo del camp ya está en la base, o está abandonado, y el general lo confirmó. El commander nunca lo usa sin la aprobación del general. Un rechazo que dice que mergees el pull request y hagas pull se responde mergeando y haciendo pull, no con --discard.