📝 Gremlins run log: block 3.2 note

This commit is contained in:
2026-10-16 20:40:20 +02:00
parent e9f1b0615a
commit 11c1085ba1
@@ -572,3 +572,44 @@ Intake 2026-10-16 18:23 +0200, a fresh (successor) Athanor, `claude-opus-5-5[1m]
- Left for later: 3.2 (`where` shape, `detach_failed`, `tree_kill_rc`, platform seam); SKILL.md and the other Phase 3 text (3.5, 3.6); workpad documents keep the old names (history, per digest §2.8).
- Expected broken: nothing known. The full suite was not run (it runs once in 3.8); `SubprocessTests` and the rest of `LaunchTests` were not run in this block.
- Commits: `01c4bc8` (rename), then this note's commit.
### Block 3.2 — Platform seam and gate
- Model: `claude-opus-5-5[1m]` (fresh block owner, own tools, no agents dispatched)
- Start / end: Fri Oct 16 20:26:43 WEDT 2026 / Fri Oct 16 20:39:15 WEDT 2026 (`date`; the end is the gate commit, this note followed)
- Done (digest §3.1–§3.7 and §4, with amendment B1, A1, A5, A7, A11, A12 and the `SPIRE_GREMLIN_UNSUPPORTED_OK` naming fix):
- **Seam** (`gremlins.py`, one section between `# --- platform seam: begin ---` and `# --- platform seam: end ---`): `Platform` (interface; shared `describe()` built from `name`, `sys.platform`, `is_wsl()`, `supported`, `detach_method`; shared `pid_alive` = platform `_running(pid)` + the creation-time check), `WindowsPlatform` (`supported = True`, `cim-hidden`; ctypes/kernel32/`PROCESSENTRY32W` loaded lazily in `_win()` on the first process or window call, never at import or in `__init__`; `find_cygpath`, `cygpath_w`, `norm_path`, `_powershell`, `_taskkill` static; `POWERSHELL_CIM`, `CREATE_NO_WINDOW`, the FILETIME epoch and the Win32 constants are class attributes), `PosixPlatform` (`setsid`; `supported` is a property `sys.platform in TESTED_SYS_PLATFORMS`, the tuple empty, so lifting the gate is one change; `is_wsl` = `WSL_DISTRO_NAME` or `microsoft` in `/proc/sys/kernel/osrelease`), `select_platform()` (the only `os.name` test), `PLATFORM`. Bodies moved without behaviour change; `restore_no_replace` now carries the `OSError → False` that `_restore` had.
- **Delegates** per §3.3: `norm_path(p, cwd, windows)` (selector over the two classes' static `norm_path`), `proc_ctime`, `epoch_to_ctime`, `pid_alive`, `process_table` → `PLATFORM.…`; `terminate(rec)` → `record_alive` then `PLATFORM.terminate`; `kill_tree` → `PLATFORM.tree_kill`; `_run` and `Supervisor.run` → `**PLATFORM.popen_kwargs()` / `(new_group=True)`; `detach_supervisor` builds the argv and returns `PLATFORM.detach(argv, cwd=state, log_path=<raw>/<job>.sup.log)`; `RegistryLock._restore` → `PLATFORM.restore_no_replace`. `process_tree` unchanged (module-level names, so `ProcessTreeTests` patches still bite). `IS_WINDOWS`, `DETACH_METHOD`, `CREATE_NO_WINDOW`/`_NOWIN`, the module ctypes block are gone.
- **Path refusals** (§3.4, A5): Windows refuses `/mnt/<d>/…` ("WSL path form on Windows; pass C:\... or /c/..."); `C:`/`C:x` stay refused ("cannot normalise path", as today); POSIX refuses a drive form ("Windows drive path form on POSIX; …") and any backslash ("backslash in a path on POSIX (a Windows path form); …"). "empty path" refusals now carry `path` (the input).
- **JSON renames** (§3.6): `where.spawn` removed → `where.platform` = `{name, sys_platform, wsl, supported, detach, override}`; `kill`/`expire.killed[]` `taskkill_rc` → `tree_kill_rc`; reason `spawn_failed` → `detach_failed`; `where` help "… git, env scrub, platform".
- **Gate** (second commit, §4 + amendments): neutral helpers `gate_overridden()` (A12: env `SPIRE_GREMLIN_UNSUPPORTED_OK == "1"` and not `PLATFORM.supported`), `platform_report()` (`describe()` + `override`), `platform_gate()` (reads only `PLATFORM.supported`/`describe()`; A7 reason "spire:gremlins is not tested on <sys_platform><, WSL> yet; use fresh agents for every block (see SKILL.md, Platforms)."), `require_platform()` (`{"error": "platform not supported", "reason", "platform"}`, exit 2). `where`: gate reason first in `reasons`, everything else still filled; override → `warnings` gets `GATE_OVERRIDE_WARNING`. A11: `where` catches a `GremlinsError` from `resolve_binary` into `binary.{path: null, how, error, given}` and `reasons`. `init`: `require_platform()` is its first line (B1). Dispatcher (A1): `GATED_COMMANDS = train, spawn, continue, rebase, tick` refused after the N3 check. `tick_pass` returns `{"pinged": [], "bases": []}` at once on a closed gate (covers the tick in `wait` and in the supervisor).
- **Tests**: `PathMatrixTests` (new, 5 tests: all 15 rows (17 inputs) × both classes, pure: `WindowsPlatform.find_cygpath` and `_run` faked, the fake asserts it is only ever called as cygpath `-w`; row 6 without cygpath; the delegate selector; both classes built on any host with `_api` still `None`). `GateTests` (new, 8 tests): `supported` flags (`WindowsPlatform` True; `PosixPlatform` False under `sys.platform` `linux` and `darwin`, `describe()["supported"]` follows), `is_wsl` (env, osrelease with/without `microsoft`, unreadable), the exact gate reason and the override, the host `where` (Windows: supported, no override, no warnings), closed `where` (exit 2, reason, the full platform dict, binary still filled), closed `init` (exit 2, `error == "platform not supported"`, no state dir; a non-`.md` `--run` still gets the gate error, so the gate precedes `state_dir_for`), override (`where` exit 0 with the one warning, `init` creates the registry), and A1 (registry made with the gate open; closed: `spawn`, `train`, `continue`, `rebase`, `tick` exit 2 `platform not supported`, registry unchanged and `jobs == {}`; `tick_pass` returns without reading the registry; `list` and `wait` still work, `pings == []`). The fake closed platform (B1) subclasses the host's own platform class (a `WindowsPlatform` here) with `supported = False`, `is_wsl() → True` and the posix/linux/WSL `describe()`. `CliTests` gains `test_where_platform` (A12: `override: false` with C13's env set; Windows: the exact platform dict) and `test_where_refused_binary_path` (A11). `Base.setUp` sets `SPIRE_GREMLIN_UNSUPPORTED_OK=1` (C13); `GateTests.setUp` pops it. `visible_windows()` in the tests → `gremlins.PLATFORM.visible_windows() or []`; `test_no_visible_window` skips on `None`/empty. `find_cygpath` patches → `gremlins.WindowsPlatform`. `test_spawn_failed` → `test_detach_failed`. Platform-behaviour branches use `WIN_PLATFORM = gremlins.PLATFORM.name == "windows"`; `IS_WINDOWS = os.name == "nt"` stays for the generated wrappers, the stub drift lines, the module-end sweep, `AtomicTests`' sharing-violation check (not in this block's classes) and `PathMatrixTests`' `select_platform` reference check.
- Changed assertion lines (old → new; `git diff 065afcb..e9f1b06 -- …/tests/ | grep -c '^-.*assert'` = 5):
- `SubprocessTests.test_where_via_uv_stub`: `assertEqual(out["spawn"], "cim-hidden" if IS_WINDOWS else "setsid")` → `assertEqual(out["platform"]["detach"], "cim-hidden" if WIN_PLATFORM else "setsid")` plus a new `assertNotIn("spawn", out)` (§3.5/§3.6: specified change, tightened).
- `LaunchTests.test_detach_failed`: `("failed", "spawn_failed")` → `("failed", "detach_failed")`; ledger `["spawn_failed"]` → `["detach_failed"]` (§3.6 rename). The fake's `"method": gremlins.DETACH_METHOD` → `gremlins.PLATFORM.detach_method` (not an assertion line).
- `CliTests.test_where_warns_on_base_url`: `assertEqual(out["warnings"], [])` → `assertEqual(out["warnings"], gate_warning(out))`; `assertEqual(len(out["warnings"]), 1, …)` → `assertEqual(len(out["warnings"]), 1 + len(gate_warning(out)), …)` plus a new `assertEqual(out["warnings"][1:], gate_warning(out))`. `gate_warning(out)` is `[]` unless `platform.override` is true, which never happens on Windows (A12), so on this host both assertions are exactly the old ones; on a porting host under C13 they demand exactly the one override warning instead of failing by construction. Not loosened.
- Non-assertion fixture change: `ProcessTreeTests.test_reap_lost_bounds_by_heartbeat`'s fake `kill_tree` returns `"tree_kill_rc"` instead of `"taskkill_rc"`.
- None dropped or loosened.
- Gates:
1. `uv run python -m py_compile …/gremlins.py …/tests/test_gremlins.py`: exit 0.
2. `uv run plugins/spire/skills/gremlins/tests/test_gremlins.py PathTests PathMatrixTests GateTests PidTests ProcessTreeTests LockTests CliTests SubprocessTests TickTests LaunchTests.test_kill_tree LaunchTests.test_no_visible_window LaunchTests.test_detach_failed LaunchTests.test_train_spawn_continue_end_to_end`: exit 0, `Ran 63 tests in 15.577s`, `OK` (no skips; `test_no_visible_window` ran with its positive control). The same list without `GateTests` passed at the seam commit (`Ran 53 tests in 15.457s`, OK). Extra, because the gate touches `init`, the dispatcher and `tick_pass`: `InitTests WarmthTests LaunchTests.test_refusals`: exit 0, `Ran 32 tests in 5.025s`, OK. Logs in the session scratchpad `…\scratchpad\gremlins-3.2\`.
3. G-where `uv run plugins/spire/skills/gremlins/gremlins.py where`: exit 0, one JSON line, `ok: true`, `platform: {"name": "windows", "sys_platform": "win32", "wsl": false, "supported": true, "detach": "cim-hidden", "override": false}`, no `spawn` key, `warnings: []`, binary 2.1.299 via `CLAUDE_CODE_EXECPATH`.
4. Seam grep §3.7 (`git grep -n -E "os\.name|sys\.platform|IS_WINDOWS|ctypes|_k32|taskkill|powershell|killpg|setsid|start_new_session|creationflags|cygpath|/proc" -- …/gremlins.py`): markers at `:267` and `:734`; every hit lies between them (`:271`, `:272`, `:282`, `:286`, `:368`–`:399`, `:405`–`:444`, `:466`–`:486`, `:501`–`:544`, `:560`–`:569`, `:585`–`:596`, `:608`–`:729`; `sys.platform` only in `Platform.describe` `:286` and `PosixPlatform.supported` `:615`, `os.name` only in `select_platform` `:729`). No hit outside, in code, comments or docstrings (the two comment/docstring mentions outside were reworded: the supervisor's `os.environ.clear()` comment and `kill_job`'s docstring say "tree-kill").
5. Leftover processes (20:39, `Get-CimInstance Win32_Process` by command line `gremlins-test-`, `_supervise`, `poscontrol`, `claude-stub`): none.
- No live `claude` calls (stub only).
- Deviations from plan:
- The fake closed platform subclasses `type(gremlins.PLATFORM)`, not `WindowsPlatform` by name: on Windows it is a `WindowsPlatform` subclass exactly as B1 asks; on a POSIX host it keeps POSIX path handling, so `GateTests` also run on WSL and the porting hosts.
- `GateTests` assert the A7 wording ("not tested on linux, WSL", "fresh agents"), not §4's "Windows only" (A7 supersedes it).
- The override opens only on the exact value `1` (§4 says "`=1`"); any other value is "not set".
- `PosixPlatform.supported` is a property over an empty `TESTED_SYS_PLATFORMS` tuple instead of a literal `False` (§4: "lifting the gate … returns True for the tested `sys.platform` values"); `describe()` reads it (A12).
- `pid_alive` is shared in `Platform` (range check and creation-time check) over a per-platform `_running(pid)`; the interface method is the same, the logic is today's.
- `where.binary` on an A11 refusal carries `given` (the refused input) besides `path: null`, `how` and `error`.
- `where`'s gate-overridden warning is a fixed constant `GATE_OVERRIDE_WARNING` (tests compare it exactly).
- Flagged for review:
- `GATED_COMMANDS` leaves `kill` and `expire` open on a closed gate (A1 names only train/spawn/continue/rebase/tick); a porter can still stop jobs started under the override. `wait` and `list` stay open too, and `_supervise` is not gated (nothing reaches it on a closed gate except a job launched under the override).
- Under the override the gremlin's own env (scrubbed) still carries `SPIRE_GREMLIN_UNSUPPORTED_OK` (unknown vars pass the scrub), so a supervisor's tick on a porting host pings as the orchestrator would; irrelevant on Windows. The 3.7 handoff may want to say so.
- `PathTests.test_posix_forms` and the other old `PathTests` stay unchanged (§3.4: redundant but kept).
- `AtomicTests` keeps its `IS_WINDOWS` check (sharing-violation retry is file-system behaviour; not one of this block's classes).
- Left for later: 3.3 (N3, N4, the kill-verification guard on `kill_tree`), the platform handoff text (3.7: the seam, `TESTED_SYS_PLATFORMS`, the override and its env-leak note above, M14), SKILL.md's Platforms section (3.5). The `!` injection with a `where` that exits 2 on a closed platform remains untested (digest §12).
- Expected broken: nothing known. The full suite was not run (it runs once in 3.8); `ScrubTests`, `AtomicTests`, `RuleTests`, `UsageTests` and the rest of `LaunchTests` were not run in this block.
- Commits: `25d0358` (seam and path matrix), `e9f1b06` (gate), then this note's commit.