🚩 gremlins: Windows-only gate in where, init and launches

This commit is contained in:
2026-10-16 20:39:15 +02:00
parent 25d03587b4
commit e9f1b0615a
2 changed files with 289 additions and 13 deletions
+73 -11
View File
@@ -119,6 +119,14 @@ USER_SETTINGS_PATH: str | None = None
PHASE2_COMMANDS = () # commands that tolerate unknown arguments (none: all are implemented)
# N3: a gremlin may only run where, list, usage and wait (`tick` launches pings).
CHILD_REFUSED = ("train", "spawn", "continue", "rebase", "kill", "expire", "init", "tick")
# The platform gate (digest §4, amendment A1): on an unsupported platform these launch nothing.
# `init` checks the gate itself, as its first step (B1); `where` reports it.
GATED_COMMANDS = ("train", "spawn", "continue", "rebase", "tick")
# Porting aid only (C12, documented in the platform handoff, not in SKILL.md): `=1` opens the
# gate for that call on an unsupported platform.
UNSUPPORTED_OK_ENV = "SPIRE_GREMLIN_UNSUPPORTED_OK"
GATE_OVERRIDE_WARNING = ("platform gate overridden (%s=1); this platform is untested, use it only "
"to port and test spire:gremlins" % UNSUPPORTED_OK_ENV)
# Warmth and keep-warm (design 5, block 2.1). Module-level so tests can shorten them.
TICK_SUP_S = 60.0 # a supervisor runs one tick pass this often
@@ -726,6 +734,38 @@ PLATFORM = select_platform()
# --- platform seam: end ---------------------------------------------------------------
# --------------------------------------------------------------------------------------
# Platform gate (digest §4): fail closed where the helper is untested
# --------------------------------------------------------------------------------------
def gate_overridden() -> bool:
"""A12: the override counts only where the gate is closed (on a supported platform the env
var changes nothing)."""
return os.environ.get(UNSUPPORTED_OK_ENV) == "1" and not PLATFORM.supported
def platform_report() -> dict:
"""`where`'s `platform` block: `PLATFORM.describe()` plus `override`."""
return {**PLATFORM.describe(), "override": gate_overridden()}
def platform_gate() -> str | None:
"""None when the platform is supported or the gate is overridden; else the reason."""
if PLATFORM.supported or gate_overridden():
return None
d = PLATFORM.describe()
return ("spire:gremlins is not tested on %s%s yet; use fresh agents for every block "
"(see SKILL.md, Platforms)." % (d.get("sys_platform"), ", WSL" if d.get("wsl") else ""))
def require_platform() -> None:
"""Refuse (exit 2) on a closed gate: `init` and the launching commands (A1)."""
reason = platform_gate()
if reason is not None:
raise GremlinsError("platform not supported", reason=reason, platform=platform_report())
# --------------------------------------------------------------------------------------
# Paths
# --------------------------------------------------------------------------------------
@@ -2216,29 +2256,45 @@ def build_parser() -> _Parser:
def cmd_where(args) -> tuple[dict, int]:
"""The preflight report. On a closed platform gate: `ok: false` with the gate reason first,
and every other part still filled (the porting hosts need it)."""
reasons = []
gate = platform_gate()
if gate is not None:
reasons.append(gate)
py_ok = sys.version_info >= (3, 10)
if not py_ok:
reasons.append("python < 3.10")
py = {"version": platform.python_version(), "executable": sys.executable,
"base_executable": getattr(sys, "_base_executable", sys.executable),
"supervisor_interpreter": supervisor_interpreter(), "ok": py_ok}
path, how = resolve_binary(args.binary)
binary = {"path": path, "how": how}
if path is None:
reasons.append("no claude binary (no --binary, no $CLAUDE_CODE_EXECPATH, none on PATH)")
try:
path, how = resolve_binary(args.binary)
except GremlinsError as e: # A11: a path form this platform refuses
path = None
how = ("--binary" if args.binary else
"CLAUDE_CODE_EXECPATH" if os.environ.get("CLAUDE_CODE_EXECPATH") else "PATH")
binary = {"path": path, "how": how, "error": e.payload["error"],
"given": e.payload.get("path", args.binary)}
reasons.append("%s: %s" % (e.payload["error"], binary["given"]))
else:
try:
binary.update(binary_info(path))
except GremlinsError as e:
binary["error"] = e.payload["error"]
reasons.append("%s: %s" % (e.payload["error"], path))
binary = {"path": path, "how": how}
if path is None:
reasons.append("no claude binary (no --binary, no $CLAUDE_CODE_EXECPATH, none on PATH)")
else:
try:
binary.update(binary_info(path))
except GremlinsError as e:
binary["error"] = e.payload["error"]
reasons.append("%s: %s" % (e.payload["error"], path))
warnings = []
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url is not None and base_url.strip().rstrip("/").lower() != DEFAULT_BASE_URL:
warnings.append("ANTHROPIC_BASE_URL is %r, not the default %s; it is passed to every "
"base and gremlin, so they authenticate and bill through that endpoint"
% (base_url, DEFAULT_BASE_URL))
if gate_overridden():
warnings.append(GATE_OVERRIDE_WARNING)
out = {
"script": script_path(),
"python": py,
@@ -2246,7 +2302,7 @@ def cmd_where(args) -> tuple[dict, int]:
"binary": binary,
"git": _git_report(),
"scrub": scrub_report(),
"platform": PLATFORM.describe(),
"platform": platform_report(),
"warnings": warnings,
"ok": not reasons,
}
@@ -2417,6 +2473,7 @@ def _diff_fields(a, b, prefix="") -> list[str]:
def cmd_init(args) -> tuple[dict, int]:
require_platform() # B1: the first step, before any path is read or written
state = state_dir_for(args.run)
snap = read_registry(state) if os.path.isdir(state) else None
old_cfg = (snap or {}).get("config")
@@ -3250,7 +3307,10 @@ def tick_pass(state: str) -> dict:
"""One pass of the ping rule over every base of the run. The decision and the `ping_in_flight`
mark are made under the registry lock (C3d), so concurrent tickers launch one ping per base.
The binary check runs before the lock for the bases that look eligible. Pings go through the
same hidden spawn as any job. Returns {"pinged": [job...], "bases": [decision...]}."""
same hidden spawn as any job. Returns {"pinged": [job...], "bases": [decision...]}.
On a closed platform gate (A1) it returns at once: no ping, nothing read or written."""
if platform_gate() is not None:
return {"pinged": [], "bases": []}
reg = read_registry(state)
if not reg or not reg.get("config"):
return {"pinged": [], "bases": []}
@@ -3977,6 +4037,8 @@ def run(argv) -> tuple[dict, int]:
if args.command in CHILD_REFUSED and in_child():
raise GremlinsError("`%s` is refused inside a gremlin (%s is set); a gremlin may only run where, "
"list, usage and wait" % (args.command, CHILD_MARKER), command=args.command)
if args.command in GATED_COMMANDS:
require_platform() # A1: an existing registry launches nothing on a closed gate
return HANDLERS[args.command](args)