¿Qué hace cierre?
Toma un PR revisado y devuelve el espacio de trabajo a una base limpia: fusiona, cambia el shell del worktree antes de eliminarlo, poda la rama fusionada, extrae la base y verifica el estado final. Este es el paso final de la tubería: el movimiento más repetido ("He terminado, fúndelo y devuélveme a una base limpia") se saca de la cabeza del usuario y se convierte en un skill.
Por Qué Existe
/execute Paso 0 crea un worktree para cada slice; hasta que existía este skill, nada eliminaba uno. Los worktrees son un stock con una entrada y sin salida, por lo que el stock aumenta sin límite (Meadows, Thinking in Systems — la solución estructural para un stock con entrada sin salida es agregar el bucle de retroalimentación faltante, no esforzarse más). La secuencia de fusión → desmontaje → extracción solo existía en la memoria del usuario, y "la memoria prospectiva falla en silencio" (Norman, The Design of Everyday Things — mueve el conocimiento de la cabeza al mundo). El orden es genuinamente propenso a errores: eliminar un worktree mientras el cwd del shell todavía está dentro de él deja el shell varado en un directorio eliminado, y un hook de Stop dependiente de node_modules que se activa desde allí emite un muro de falsos "errores de lint/tipo" MODULE_NOT_FOUND que parecen una regresión real. El orden de cd-a-base-antes-de-eliminar en el Paso 4 es la corrección estructural para ese error exacto.
Este skill orquesta herramientas existentes (gh pr merge, wt remove / git worktree remove, commit-commands:clean_gone). No introduce estado en el sistema de archivos. Verifica el resultado — árbol limpio, en una base actualizada, PR fusionado — no simplemente que la limpieza se ejecutó (el "buscar el objetivo equivocado" de Meadows: un cierre medido por "si se ejecutó" optimiza el proxy y produce ceremonia frágil).
Posición de Invocación
Este es un skill de tubería principal que posee la cola merge → cleanup de la ruta de entrega predeterminada:
… → /pre-merge → /compound (en el PR, cuando existe una lección) → /pre-merge (re-ejecutar sobre el commit compuesto; re-sella) → [fusión + desmontaje del worktree — ESTE SKILL] → [cierre de issues — Paso 9 en prosa]
Usa /closeout cuando:
/pre-mergeha creado un PR y la revisión está completa (o el cambio es lo suficientemente trivial como para no necesitar una)- el PR está aprobado y se puede fusionar, y deseas que se fusione y el espacio de trabajo vuelva a una base limpia
- el usuario solicita "cerrar", "fusionar y limpiar", "desmontar el worktree" o "volver a una base principal/limpia"
No uses /closeout cuando:
- el PR aún no se ha creado o todavía está en revisión — ejecuta
/pre-mergeprimero - deseas capturar una lección duradera — eso es
/compound, que por defecto ya ha viajado en el PR antes de que/closeoutlo fusione (el/compoundposterior a la fusión es el plan de respaldo);/closeoutno captura lecciones - deseas cerrar el PRD y los issues del slice o reconciliar el frontmatter del artifacto de investigación — eso es la prosa de limpieza en
SYSTEM-OVERVIEW.mdPaso 9;/closeoutrealiza la mitad de higiene de git y difiere la mitad de cierre de issues y supersesión del artifacto de investigación al Paso 9 en lugar de duplicarla. El único cierre que realiza es el elemento de trabajo del slice fusionado en el backendsidecar(Paso 3), porque ninguna líneaCloses #Nlo hace en la fusión - el cambio no está en una rama de características en absoluto (estás en la rama base sin nada que fusionar) — no hay nada que cerrar
Está confirmado por HITL y no es bloqueante: operacionaliza una cola ya opcional. Nunca auto-fusiona — la fusión está orientada al exterior y es difícil de revertir, por lo que siempre confirma primero.
Proceso
Ejecuta esto como una lista de verificación de hacer-confirmar (Gawande): realiza cada paso, luego verifica su resultado antes del siguiente. Los pasos destructivos (4–6) están autoprotegidos: confirma las condiciones previas de seguridad en el Paso 2 antes de ejecutarlos.
0. ¿Aislamiento aprovisionado por el host? Ceder el desmontaje primero
Antes de la secuencia de fusión/desmontaje, determina si el aislamiento de este repositorio es propiedad del host (Conductor, Codespaces, devcontainers). Cuando lo es, /closeout todavía realiza la fusión nativa de GitHub pero no desmonta el worktree ni poda la rama local — el host posee esa mitad del ciclo de vida, y luchar contra ella es la resistencia a políticas de Meadows (dos actores empujando el mismo stock). Cede el stock del worktree al host.
La detección de salida es más estricta que la verificación de entrada de /execute y no debe usar la heurística genérica "toplevel ≠ árbol de trabajo primario" por sí sola: un worktree hecho por la tubería también satisface esa prueba, por lo que la señal genérica no puede distinguir "el host posee el desmontaje" de "la tubería hizo esto y debe desmontarlo". Resuelve desde .claude/settings.json worktree.provisioning (por defecto "auto" cuando está ausente), y honra una anulación explícita en este extremo exactamente como /execute lo hace en la entrada:
host— cede el desmontaje incondicionalmente.pipeline— la tubería posee el desmontaje. Ejecuta los Pasos 4–6 completos a continuación incluso si una variable de entorno del host está presente — una configuración explícita depipelinesignifica que/executeaprovisionó el worktree por sí mismo, por lo que/closeoutdebe desmontarlo (el comportamiento anterior al host). Una variable de entorno del host no anula una elección explícita depipeline.auto(por defecto) — cede el desmontaje solo si una variable de entorno del host describe este árbol: el bloque a continuación imprimehost_owned=yes. Nunca ceder por la señal genérica "toplevel ≠ primaria" sola, y nunca por la mera presencia deCONDUCTOR_WORKSPACE_PATH— un shell iniciado bajo Conductor lo hereda y puede llevar uno que nombre el espacio de trabajo de un repositorio diferente, y ceder por eso deja un worktree hecho por la tubería sin que nadie lo desmonte.
# señal-del-host: ¿una variable de entorno del host describe ESTE árbol, no solo este shell?
host_owned=no
top=$(cd "$(git rev-parse --show-toplevel)" && pwd -P)
if [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ]; then
ws=$(cd "$CONDUCTOR_WORKSPACE_PATH" 2>/dev/null && pwd -P) || ws=
if [ -n "$ws" ]; then
case "$top/" in "$ws"/*) host_owned=yes ;; esac
fi
fi
if [ -n "${CODESPACES:-}" ] || [ -n "${REMOTE_CONTAINERS:-}" ]; then host_owned=yes; fi
echo "host_owned=$host_owned"
Si el desmontaje no se cede (incluyendo worktree.provisioning: "auto" con host_owned=no), la tubería lo posee — ejecuta los Pasos 4–6 completos a continuación como están escritos.
Cuando el aislamiento es propiedad del host, /closeout ejecuta una secuencia reducida:
- El Paso 3 (fusión) todavía se ejecuta en su totalidad —
gh pr merge --delete-branch. Conductor y hosts similares crean el PR pero dejan la fusión a una acción humana; la tubería que realiza la fusión nativa de GitHub es el valor que agrega, y el host refleja el resultado. La deferencia se limita al desmontaje del sistema de archivos, nunca a la fusión. - Omitir el reanclaje del Paso 4,
wt remove/git worktree remove/ExitWorktree { remove }del Paso 5 y la poda de la rama local del Paso 6 — el host posee el ciclo de vida del worktree y la rama (Conductor los reclama cuando el espacio de trabajo se archiva). Nunca usar--forcecontra un árbol administrado por el host. - Lista de verificación del Paso 8: "Worktree eliminado" y "Rama podada" se convierten en "N/A — administrado por el host." Las comprobaciones de fusión, árbol limpio, PR fusionado y rama remota aún se aplican. La última es fácil de archivar bajo el mismo N/A y no lo es: lo que el host posee es el worktree del sistema de archivos y la rama local, mientras que la rama remota se elimina con
gh pr merge --delete-branchque este skill aún ejecuta en su totalidad una viñeta arriba. Ceder la mitad que el host posee no cede la mitad que no posee.
1. Confirmar intención y recopilar estado
Nunca auto-fusionar. Declara lo que estás a punto de hacer y obtén una autorización explícita.
Recopila los hechos primero (estos son de solo lectura):
git branch --show-current # la rama de características
git rev-parse --show-toplevel # el árbol de trabajo actual (puede ser un worktree)
git worktree list --porcelain | awk '/^worktree /{print $(2); exit}' # el checkout BASE (árbol de trabajo principal)
gh pr view --json number,title,url,state,mergeable,mergeStateStatus,reviewDecision 2>/dev/null
Determina la rama base a la que apunta el PR — la predeterminada declarada del repositorio, no una suposición:
SC="<directorio de este skill>/references/sidecar.sh"
bash "$SC" base-ref --name # el nombre de la rama simple: prod, nunca origin/prod
Este sitio quiere el nombre de la rama base, no una ref — alimenta git switch <rama-base> en el Paso 4, y git switch rechaza de plano una ref de seguimiento remoto (fatal: se esperaba una rama, se obtuvo la rama remota 'origin/prod') — quiere la rama base local que estás a punto de extraer. Por lo tanto, solicita base-ref --name, y deliberadamente no la forma de ref que toman /pre-merge, /help, /walk-commits, /visual-recap, /compound y /execute; necesitan una ref solo porque usan la base como un punto final de rango, que este skill nunca hace. No "arregles" esto para que coincida con ellos. Si base-ref --name sale con un código distinto de cero, no ha encontrado ninguna base: transmite su stderr y detente en lugar de adivinar una.
Informa al usuario, en un bloque corto: qué PR se fusionará, qué worktree (si lo hay) y rama se desmontarán, y a qué rama base regresarás y extraerás. Espera confirmación. Si no hay ningún PR en esta rama, detente y redirige a /pre-merge.
2. Verificar condiciones previas de seguridad antes de cualquier paso destructivo
El desmontaje es destructivo; verifica que sea seguro primero (autoprotegido). Todos estos deben cumplirse:
- La rama de características está completamente enviada —
git statusno muestra commits sin enviar, ygit log @{u}..está vacío. - Existe un PR para esta rama y es fusionable (
mergeableesMERGEABLE;mergeStateStatusno esBLOCKED/DIRTY). - Las comprobaciones requeridas están en verde y la revisión está aprobada (
reviewDecisionesAPPROVED, o el repositorio no requiere revisión y el usuario lo ha aceptado explícitamente).
Si alguna condición previa falla, detente e informa cuál — no fusiones ni elimines nada. Una condición previa fallida es una señal para regresar a la revisión, no para forzar el paso. Cuando la que falla es la revisión — reviewDecision es CHANGES_REQUESTED — el camino de regreso es /respond-to-review: verifica cada comentario contra el código, el humano decide qué arreglar y qué rechazar, y las correcciones pasan por /fix-findings y una re-ejecución de /pre-merge antes de que /closeout se ejecute nuevamente.
Actualidad de la revisión: ¿la aprobación aún cubre este diff? (no bloqueante)
Las tres casillas de verificación anteriores preguntan si ocurrió una revisión. Ninguna de ellas pregunta si cubrió el diff que se va a fusionar. En el flujo lineal previsto, esas son la misma pregunta, que es exactamente por qué la brecha es invisible; en una sesión HITL real, el alcance se acumula en una rama después de su paso de revisión, y nada conecta el diff que /pre-merge leyó con el diff que /closeout fusiona. Eso es una entrada sin bucle de retroalimentación (Meadows — la solución estructural es agregar el bucle, no exhortar), y degrada silenciosamente "el PR fue revisado" a "una versión temprana del PR fue revisada" en el momento en que la degradación más importa. Continuous Delivery establece la regla claramente: el artifacto que revisaste debe ser el artifacto que envías.
Lee el sello que /pre-merge Fase 4 dejó y compáralo con la cabeza actual. Vive donde backend stamp dice, según references/state-backend.md: en el cuerpo del PR, o en el sidecar.
git fetch --quiet
SC="<directorio de este skill>/references/sidecar.sh"
case "$(bash "$SC" backend stamp)" in
sidecar)
REVIEWED_SHA=$(bash "$SC" stamp read "$(git branch --show-current)")
HEAD_SHA=$(git rev-parse '@{u}' 2>/dev/null || git rev-parse HEAD) ;;
github)
REVIEWED_SHA=$(gh pr view <número> --json body -q .body \
| sed -n 's/.*<!-- reviewed-at: \([0-9a-f]\{40\}\) -->.*/\1/p' | tail -1)
HEAD_SHA=$(gh pr view <número> --json headRefOid -q .headRefOid) ;;
*) echo "closeout abortado: sidecar.sh backend stamp falló; no se adivinará la ruta de github" >&2; exit 1 ;;
esac
En el backend sidecar, la cabeza es el upstream de la rama, que es lo que la forja fusionará: la primera condición previa anterior ya requiere que coincida con la cabeza local. Cae a HEAD cuando la rama no tiene upstream. stamp read imprime el mismo OID completo de 40 caracteres que lleva el marcador del cuerpo del PR, o nada, por lo que todo a continuación se ejecuta sin cambios en cualquier backend.
Exactamente 40 caracteres hexadecimales, comparados por igualdad de cadenas. Ambas mitades de esa oración son estructurales, porque este marcador es un contrato que abarca dos skills. /pre-merge Fase 4 emite el OID completo de 40 caracteres, y headRefOid siempre tiene 40 caracteres, por lo que \{40\} es el único ancho que puede compararse igual — aceptar uno más corto permitiría que un marcador mal formado (un cuerpo editado a mano, un marcador de posición de plantilla sustituido en la mitad equivocada) se analice correctamente y luego difiera de $HEAD_SHA para siempre. [[ "$REVIEWED_SHA" == "$HEAD_SHA" ]] es la comparación: igualdad exacta de cadenas en dos OID completos, nunca una prueba de prefijo. Un marcador que la expresión regular rechaza produce un $REVIEWED_SHA vacío y toma la rama "no se encontró sello" a continuación, que se degrada con gracia — mucho mejor que la rama "los SHA difieren" que grita lobo en un PR cuya cabeza nunca se movió. En el repositorio Skill Kit, scripts/test-review-currency-marker.sh fija este viaje de ida y vuelta contra ambos skills para que el formato del escritor y este analizador no puedan separarse silenciosamente.
Tres resultados, y solo uno de ellos interrumpe:
- Los SHA coinciden — OID completos iguales en cadena, por lo que el diff revisado es el candidato a fusión. Pasa silenciosamente. No lo narres: este es el caso común, y anunciar un no evento cada ejecución es lo que convierte una puerta en papel tapiz.
- No se encontró sello (
$REVIEWED_SHAvacío) — un PR creado a mano, una contribución externa, un PR anterior al sello o un marcador cuya forma la expresión regular anterior rechazó. Anótalo en una línea ("sin sello/pre-mergeen este PR — actualidad de la revisión desconocida") y continúa. La ausencia de un sello no es evidencia de un diff no revisado, y detenerse en cada PR sin sello es precisamente la fatiga de alarma que hace que una puerta sea derrotada reflexivamente (Meadows, resistencia a políticas). - Los SHA difieren — dos OID de 40 caracteres bien formados que no son iguales en cadena, lo que significa que los commits llegaron después de la revisión. No te detengas...
[truncado para la traducción]