feat: terminal launcher diagnostics and mutation preflight (Closes #556)

Add categorize-and-probe helpers for shell spawn failures, expose
gitea_diagnose_terminal, and fail closed on mutation capability resolve
when the terminal launcher is unhealthy (BLOCKED + DIAGNOSE). Document
the operator path in the workflow runbooks.
This commit is contained in:
2026-07-09 10:29:03 -04:00
parent cc4b95839d
commit 223cde1b94
5 changed files with 340 additions and 2 deletions
+154 -1
View File
@@ -8,6 +8,9 @@ available unless explicit recovery-mode proof is supplied.
from __future__ import annotations
import re
import os
import shutil
import subprocess
from typing import Any
from reviewer_fallback import LOCAL_GITEA_SCRIPT_NAMES
@@ -366,4 +369,154 @@ def assess_gitea_operation_path(
else "proceed with native MCP"
)
),
}
}
def default_terminal_probe_command() -> list[str]:
return ["git", "--version"] if shutil.which("git") else ["echo", "ok"]
def _resolve_executable(cwd: str | None, executable: str | None) -> str | None:
if not executable:
return None
if os.path.isabs(executable):
return executable if os.path.exists(executable) and os.access(executable, os.X_OK) else None
if "/" in executable or "\\" in executable:
target_cwd = cwd or os.getcwd()
full_path = os.path.abspath(os.path.join(target_cwd, executable))
return full_path if os.path.exists(full_path) and os.access(full_path, os.X_OK) else None
return shutil.which(executable)
def diagnose_terminal_failure(cwd: str | None, command: list[str]) -> dict[str, Any]:
"""Inspect and categorize command spawn failures (#556)."""
diagnostics = {
"cwd": cwd,
"command": command,
"cwd_exists": True,
"cwd_is_dir": True,
"shell_exists": True,
"executable_exists": True,
"resolved_executable": None,
"error_type": "session launcher failure",
"error_msg": (
"Subprocess spawn failed despite valid CWD and executable. This may be due "
"to sandboxing, permission restrictions, or system resource limits."
),
}
if cwd:
if not os.path.exists(cwd):
diagnostics["cwd_exists"] = False
diagnostics["error_type"] = "missing cwd"
diagnostics["error_msg"] = f"Current working directory does not exist: {cwd}"
return diagnostics
if not os.path.isdir(cwd):
diagnostics["cwd_is_dir"] = False
diagnostics["error_type"] = "cwd is not a directory"
diagnostics["error_msg"] = f"Current working directory is not a directory: {cwd}"
return diagnostics
exe = command[0] if command else None
if exe:
resolved_exe = _resolve_executable(cwd, exe)
diagnostics["resolved_executable"] = resolved_exe
if not resolved_exe:
diagnostics["executable_exists"] = False
if "venv" in exe or "pytest" in exe:
diagnostics["error_type"] = "missing runtime wrapper"
diagnostics["error_msg"] = (
"Virtual environment runtime wrapper/executable not found or not "
f"executable: {exe}"
)
else:
diagnostics["error_type"] = "missing executable"
diagnostics["error_msg"] = f"Executable not found in PATH or CWD: {exe}"
return diagnostics
# Check common shell availability
has_any_shell = False
for shell_path in ("/bin/sh", "/bin/zsh", "/bin/bash"):
if os.path.exists(shell_path) and os.access(shell_path, os.X_OK):
has_any_shell = True
break
if not has_any_shell:
diagnostics["shell_exists"] = False
diagnostics["error_type"] = "missing shell"
diagnostics["error_msg"] = "No standard system shell (/bin/sh, /bin/zsh, /bin/bash) is available or executable."
return diagnostics
return diagnostics
def probe_terminal_spawn(
cwd: str | None = None,
command: list[str] | None = None,
) -> dict[str, Any]:
"""Proactively probe command spawning to detect launcher health (#556)."""
probe_cmd = command or default_terminal_probe_command()
diag = diagnose_terminal_failure(cwd, probe_cmd)
if diag["error_type"] != "session launcher failure":
return {
"healthy": False,
"error_type": diag["error_type"],
"error_msg": diag["error_msg"],
"cwd": cwd,
"command": probe_cmd,
"diagnostics": diag,
}
try:
proc = subprocess.run(
probe_cmd,
capture_output=True,
text=True,
cwd=cwd,
timeout=5,
)
if proc.returncode == 0:
return {
"healthy": True,
"error_type": None,
"error_msg": "",
"cwd": cwd,
"command": probe_cmd,
"diagnostics": diag,
}
# Non-zero status still proves the launcher spawned the process.
return {
"healthy": True,
"error_type": None,
"error_msg": "",
"cwd": cwd,
"command": probe_cmd,
"exit_code": proc.returncode,
"diagnostics": diag,
}
except FileNotFoundError:
return {
"healthy": False,
"error_type": diag["error_type"],
"error_msg": diag["error_msg"],
"cwd": cwd,
"command": probe_cmd,
"diagnostics": diag,
}
except subprocess.TimeoutExpired as exc:
return {
"healthy": False,
"error_type": "probe timeout",
"error_msg": f"Subprocess probe timed out after {exc.timeout} seconds.",
"cwd": cwd,
"command": probe_cmd,
"diagnostics": diag,
}
except Exception as exc:
return {
"healthy": False,
"error_type": diag["error_type"],
"error_msg": f"Subprocess spawn failed with exception: {exc}",
"cwd": cwd,
"command": probe_cmd,
"diagnostics": diag,
}