@@ -598,6 +598,12 @@ const AUTO_MAX_ROUNDS: usize = 32;
598598/// ya emitido): antes esto mataba el turno entero — la queja nº 1 del usuario.
599599const MAX_STREAM_RETRIES : usize = 2 ;
600600
601+ /// Rondas EXTRA que el green-gate fuerza cuando el modelo intenta cerrar con el
602+ /// build/tests en rojo. Acotado: si tras estos intentos sigue rojo, se cierra
603+ /// con un aviso ROJO honesto en vez de fingir verde (mejor "incompleto" claro
604+ /// que un falso "listo"). No queremos un bucle infinito contra un fallo real.
605+ const GATE_MAX_FORCED : usize = 3 ;
606+
601607/// Resultado de un turno completo.
602608enum TurnOutcome {
603609 /// Respuesta (posiblemente parcial) del asistente, para memoria.
@@ -790,6 +796,12 @@ async fn run_turn(
790796 let mut round = 0usize ;
791797 let mut round_budget = MAX_TURN_ROUNDS ;
792798 let mut stream_retries = 0usize ;
799+ // GREEN-GATE: veredicto determinista de la ÚLTIMA verificación automática
800+ // (build/tests que inyecta dpx). `true` = quedó en rojo. Bloquea el cierre
801+ // del turno hasta que vuelva a verde. `gate_forced` acota cuántas rondas
802+ // extra forzamos para que un build irreparable no cicle eternamente.
803+ let mut verify_red = false ;
804+ let mut gate_forced = 0usize ;
793805 // Checkpoint del turno: captura el estado de cada archivo antes de tocarlo
794806 // (vía fs::apply/delete_file) y lo apila al terminar para que `/undo` pueda
795807 // revertir TODO lo que dpx escribió este turno. Se commitea al soltar el
@@ -1035,6 +1047,9 @@ async fn run_turn(
10351047 // tenga que pedirlo. Se omite si el modelo ya pidió el mismo build tool.
10361048 let mut auto_built = false ;
10371049 let mut auto_tested = false ;
1050+ // Frontera: lo que va DESPUÉS de este índice en `runs` es verificación
1051+ // que inyectó dpx (no comandos del modelo) → es lo que vigila el gate.
1052+ let runs_before_verify = runs. len ( ) ;
10381053 if crate :: fs:: touches_build ( & writes)
10391054 || crate :: fs:: edits_touch_build ( & edits)
10401055 || crate :: fs:: touches_build ( & s_writes)
@@ -1083,10 +1098,20 @@ async fn run_turn(
10831098 && round >= round_budget
10841099 && !extend_rounds ( & mut * ask, round, auto, & mut round_budget)
10851100 {
1086- println ! (
1087- "{}" ,
1088- ui:: dim( "⏸ turno detenido · el plan y la memoria quedan guardados para retomar" )
1089- ) ;
1101+ // Si encima la verificación quedó en ROJO, no lo disfraces de pausa
1102+ // tranquila: dilo claro para que el usuario sepa que el build/tests
1103+ // siguen rotos al detenerse.
1104+ if verify_red {
1105+ println ! (
1106+ "{}" ,
1107+ ui:: red( "🔴 turno detenido CON EL BUILD/TESTS EN ROJO · revisa y retoma" )
1108+ ) ;
1109+ } else {
1110+ println ! (
1111+ "{}" ,
1112+ ui:: dim( "⏸ turno detenido · el plan y la memoria quedan guardados para retomar" )
1113+ ) ;
1114+ }
10901115 break ;
10911116 }
10921117 if wants_more {
@@ -1132,7 +1157,14 @@ async fn run_turn(
11321157 } else if auto_built {
11331158 println ! ( "\n {}" , ui:: dim( "dpx verifica el proyecto (compila/linter)…" ) ) ;
11341159 }
1135- for cmd in & runs {
1160+ // Si esta ronda corre verificación automática, partimos de VERDE y
1161+ // cualquier comando de verificación que falle lo pone en rojo (OR de
1162+ // todos: clippy Y tests deben pasar). Si la ronda no verifica, el
1163+ // veredicto previo se conserva (un read no "limpia" un build roto).
1164+ if auto_built || auto_tested {
1165+ verify_red = false ;
1166+ }
1167+ for ( idx, cmd) in runs. iter ( ) . enumerate ( ) {
11361168 match confirm_run ( & mut * ask, store, cwd, cmd, auto) {
11371169 RunDecision :: Blocked ( reason) => {
11381170 ctx. push_str ( & format ! (
@@ -1148,7 +1180,7 @@ async fn run_turn(
11481180 }
11491181 RunDecision :: Run => { }
11501182 }
1151- let ( out_text, cancelled) = execute_run ( cwd, cmd) ;
1183+ let ( out_text, cancelled, exit_code ) = execute_run ( cwd, cmd) ;
11521184 ctx. push_str ( & format ! ( "\n --- salida de `{cmd}` ---\n {out_text}\n --- fin ---\n " ) ) ;
11531185 if cancelled {
11541186 ui:: clear_cancel ( ) ;
@@ -1159,6 +1191,14 @@ async fn run_turn(
11591191 TurnOutcome :: Reply ( full)
11601192 } ;
11611193 }
1194+ // GREEN-GATE: si este comando es la verificación que inyectó dpx
1195+ // (no uno del modelo) y su exit code no fue 0, el turno queda en
1196+ // rojo. `Some(0)` es la única señal de verde; `None` (ni arrancó)
1197+ // o cualquier otro código cuenta como rojo.
1198+ let is_auto_verify = ( auto_built || auto_tested) && idx >= runs_before_verify;
1199+ if is_auto_verify && exit_code != Some ( 0 ) {
1200+ verify_red = true ;
1201+ }
11621202 }
11631203 // Si solo hubo tool calls nativas, sus resultados ya viajan en el
11641204 // historial como tool results: el prompt es solo la instrucción.
@@ -1176,6 +1216,32 @@ async fn run_turn(
11761216 } ;
11771217 continue ;
11781218 }
1219+ // 🟢 GREEN-GATE: el modelo no pidió más acciones → se da por terminado.
1220+ // Pero si la última verificación automática quedó en ROJO, no cerramos en
1221+ // falso: lo empujamos a una ronda de corrección. Acotado por
1222+ // `GATE_MAX_FORCED` para no ciclar contra un fallo que no sabe resolver;
1223+ // agotados los intentos, cerramos con un aviso ROJO honesto (mejor un
1224+ // "incompleto" claro que un falso "listo" con el build roto).
1225+ if verify_red {
1226+ if gate_forced < GATE_MAX_FORCED {
1227+ gate_forced += 1 ;
1228+ println ! (
1229+ "{}" ,
1230+ ui:: red( "🔴 build/tests en ROJO · no cierro el turno hasta verde" )
1231+ ) ;
1232+ to_send = "[GREEN-GATE: la última verificación automática (build/tests) quedó en \
1233+ ROJO, pero diste el turno por terminado. NO lo está: vuelve a los errores \
1234+ reportados arriba y arréglalos hasta que el proyecto compile y los tests \
1235+ pasen en verde. Si un error no lo causaste tú o no se puede arreglar, dilo \
1236+ EXPLÍCITAMENTE y explica por qué; no lo dejes pasar en silencio.]"
1237+ . to_string ( ) ;
1238+ continue ;
1239+ }
1240+ println ! (
1241+ "{}" ,
1242+ ui:: red( "🔴 INCOMPLETO · el build/tests sigue en rojo tras varios intentos de arreglo" )
1243+ ) ;
1244+ }
11791245 break ;
11801246 }
11811247
@@ -1435,7 +1501,7 @@ async fn run_tool_call(
14351501 ToolOutcome :: Done ( format ! ( "[el usuario rechazó ejecutar `{command}`]" ) )
14361502 }
14371503 RunDecision :: Run => {
1438- let ( out, cancelled) = execute_run ( cwd, & command) ;
1504+ let ( out, cancelled, _exit ) = execute_run ( cwd, & command) ;
14391505 if cancelled { ToolOutcome :: Cancelled ( out) } else { ToolOutcome :: Done ( out) }
14401506 }
14411507 } ,
0 commit comments