📝 Gremlins run log: block 3.3 note
This commit is contained in:
@@ -613,3 +613,39 @@ Intake 2026-10-16 18:23 +0200, a fresh (successor) Athanor, `claude-opus-5-5[1m]
|
||||
- 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.
|
||||
|
||||
### Block 3.3 — N3, N4, kill flake
|
||||
|
||||
- Model: `claude-opus-5-5[1m]` (fresh block owner, own tools, no agents dispatched)
|
||||
- Start / end: Fri Oct 16 20:41:17 WEDT 2026 / Fri Oct 16 21:43:31 WEDT 2026 (`date`; the end is the code commit, this note followed)
|
||||
- Done (digest §5 with amendment A2, A3, A4 and the C15 note):
|
||||
- **N3** (`gremlins.py`): `resolve_binary` now walks `BINARY_SOURCES = ("CLAUDE_CODE_EXECPATH", "PATH")` through the new `resolve_binary_via(how)` (one source only; same lookup and order as before). New `rerun_binary(stored, how, warnings)`, used by `build_init_config` on a re-run without `--binary`: a stored source `--binary` (or none recorded) keeps the stored path; `CLAUDE_CODE_EXECPATH` / `PATH` re-resolve through that source only (A4, never the other one); nothing found there keeps the stored path with the warning "could not re-resolve the claude binary through <source>; kept the stored path <path>"; a different path (compared `os.path.normcase`d) replaces it with "binary changed: <old> → <new> (resolved by <source>); existing bases keep their own binary record, new trainings use the new one". `init` output always carries `warnings` (list). The change shows in `changed_fields` as `launch.binary.*` (unchanged logic); a base keeps its own `launch.binary`. A vanished stored file still fails loud (`claude binary not found`, exit 2) when nothing re-resolves it.
|
||||
- **N4**: new `_rebase_target_free(reg, entry)` (the existing "name free or failed" rule, shared). `_rebase_succeeded`: when the old base is not `active` at finalise, NEW is created with the old base's status (`superseded` with the old base's `superseded_by` and `superseded_at` = this end time; `retired` with `retired_at` = this end time), the old base is left untouched (its `superseded_by` is not overwritten), and the job entry gets `rebase_inactive_parent: "<old> was <status> when the rebase ended"`. `_conclude` adds the flag `parent_inactive` before the result is built, only when the rebase ended `ok`, the base is not active **and** `_rebase_target_free` holds (so not on the `rebase_conflict` early return, C15 note). Status stays `ok`.
|
||||
- **Kill verification (A2)**: `kill_tree`'s sweep takes one module-level `process_table()` per iteration and asks the new `snapshot_survivor(p, table)`: dead if the table is non-empty and lacks the pid, or if the snapshot name is known, the table names the pid differently and `proc_ctime(pid)` is `None` (class (a)); otherwise `record_alive` decides, as before. Survivors are still terminated only from that list. `KILL_VERIFY_S` stays 10 (no (b) observed).
|
||||
- **Tests** (`test_gremlins.py`): `LaunchTests.test_refusals`' kill assertion now prints the output. `InitTests`: `test_rerun_reresolves_execpath`, `test_rerun_keeps_a_binary_pin`, `test_rerun_without_a_source_keeps_the_stored_path` (A3 first case), `test_rerun_with_the_stored_file_gone_fails_loud` (A3 second case), `test_rerun_never_crosses_sources` (A4, both directions: EXECPATH-stored with only a PATH claude → kept + warning; PATH-stored with EXECPATH set → re-resolved by PATH); helpers `named_stub` (a `claude.cmd` for PATH lookups), `bare_init`, `rerun`, `path_only`, `stored_binary`. `ProcessTreeTests` (fake table, module-level patches; `terminate`, `process_tree`, `PLATFORM.tree_kill` faked, nothing real touched): `test_kill_verify_pid_reused_by_unopenable_process_is_dead` (class (a)), `…_pid_gone_from_a_nonempty_table_is_dead`, `…_same_name_unverifiable_survives`, `…_empty_table_keeps_the_survivor`, `…_unknown_snapshot_name_keeps_the_survivor`, `…_other_name_with_readable_ctime_is_record_alive` (the four A2 cases plus two). `LaunchTests.test_late_rebase_of_a_superseded_base` (N4 per §5.2, plus a second rebase whose new name is taken before it ends: `ok`, no `parent_inactive`, `rebase_conflict` set, nothing replaced). `RuleTests.test_late_rebase_of_a_retired_base` (pure: the `expire` race, NEW `retired`, old untouched). The stream stub gained a `gate=NAME` directive (first turn waits, at most 120 s, for `<stub dir>/NAME`), so the late-rebase ordering is deterministic rather than timed.
|
||||
- Fail-first check: the new tests run against a scratch copy of the pre-change `gremlins.py` (`git archive HEAD` of the skill dir with the new test file, under `…\scratchpad\gremlins-3.3\pre\`): `Ran 14 tests`, `FAILED (failures=3, errors=6)`. Failing: all four new N3 behaviour tests except the A3 loud case (errors: `KeyError: 'warnings'`), `test_init_creates_state_and_config` (`warnings` missing), both class-(a)/gone-pid kill tests (`([900051], [900051, 900051, 900051]) != ([], [])`), the retired unit test (no `_rebase_target_free`), the N4 launch test (`'parent_inactive' not found in []`). Passing on the old code, as intended (guards of unchanged behaviour): `test_rerun_keeps_a_binary_pin`'s path logic, `test_rerun_with_the_stored_file_gone_fails_loud`, and the four A2 "keep the survivor" cases. To see past the `KeyError`, the N3 tests also ran against the old code with only `"warnings": []` added to `init`'s output: `Ran 6`, `failures=3` (`test_rerun_reresolves_execpath`: path stays A; `…_without_a_source_…`: no warning; `…_never_crosses_sources`: no warning); pin, loud case and the always-present key pass.
|
||||
- Kill flake (§5.3): **not reproduced**. 16 hidden Python busy loops (`gremlins33_busyload.py` in `…\scratchpad\gremlins-3.3\`, 16 logical CPUs, pids 37364 13568 38608 18780 29236 8852 20324 33132 18960 36196 35980 3244 15556 35944 15476 38032, started 21:36:17, all confirmed running, killed by pid with `taskkill /F` after the runs). `LaunchTests.test_refusals` alone, 10 sequential runs (never two suites at once), against the **pre-change** helper (so a class-(a) survivor would not be hidden by the new guard) with scratch-only instrumentation in that copy's `kill_tree` (on survivors: table name, `proc_ctime`, `_running`, a terminate retry, and up to 30 s of follow-up liveness, to a diag file): 10 of 10 `OK` (19.4, 19.4, 65.6, 23.3, 20.2, 31.3, 23.3, 27.8, 20.1, 23.0 s), no diag line written, so no survivor in any of the 20 kills. Class: none observed; survivor names: none. Fix landed: only the A2 guard; `KILL_VERIFY_S` unchanged; nothing for (c). The reproduction took about 6 minutes of the 15-minute bound.
|
||||
- Changed assertion lines (old → new; `git diff 11c1085..62eef3b -- …/tests/ | grep -c '^-.*assert'` = 1):
|
||||
- `LaunchTests.test_refusals`: `self.assertTrue(self.cmd("kill", "--job", j)["ok"])` → `out = self.cmd("kill", "--job", j)` + `self.assertTrue(out["ok"], out)` (§5.3 step 1; same check, adds the message).
|
||||
- Added: `InitTests.test_init_creates_state_and_config` `assertEqual(out["warnings"], [])`; every assertion of the new tests above.
|
||||
- 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 InitTests ProcessTreeTests RuleTests LaunchTests.test_refusals LaunchTests.test_kill_tree LaunchTests.test_expire_kills_running_jobs LaunchTests.test_late_rebase_of_a_superseded_base`: exit 0, `Ran 51 tests in 15.423s`, `OK` (`RuleTests` because this block added a test there). Logs in `…\scratchpad\gremlins-3.3\` (`gate.txt`, `pre-run.txt`, `pre-mutant.txt`, `repro-1.txt` … `repro-10.txt`).
|
||||
3. G-where `uv run plugins/spire/skills/gremlins/gremlins.py where`: exit 0, one JSON line, `ok: true`, `platform.supported: true`, `override: false`, `warnings: []`, binary 2.1.299 via `CLAUDE_CODE_EXECPATH`.
|
||||
4. Leftover processes (21:43, `Get-CimInstance Win32_Process` by command line `gremlins-test-`, `_supervise`, `poscontrol`, `claude-stub`, `gremlins33_busyload`): none.
|
||||
- No live `claude` calls (stub only).
|
||||
- Deviations from plan:
|
||||
- The reproduction ran against the pre-change helper with scratch instrumentation (the brief says only "run `test_refusals` alone"): with the A2 guard in place a class-(a) survivor would have been dropped silently, so the classification needed the old code. The repo test file (with the printing assertion) was used as is.
|
||||
- N4 test: the ordering "train ends first" is made deterministic with a new stub directive `gate=NAME` instead of a stub `sleep`; and the test also covers the `rebase_conflict` path of the C15 note (a second base, B2, rebased to a name taken by a `clone_base` before the gate opens).
|
||||
- N4 retired branch: `retired_at` of NEW is this end time (§5.2 names the field, not its value; `superseded_at` is the end time too).
|
||||
- The test asserting "B1 untouched" compares B1's lineage fields (`status`, `superseded_by`, `superseded_at`): `_conclude` still records the rebase's first request as a hit on B1 (`last_hit`, `ttl_s`, `ttl_from`), as for any rebase; that is unchanged behaviour, not part of N4.
|
||||
- `test_rerun_never_crosses_sources` compares PATH-resolved paths `os.path.normcase`d: on Windows `shutil.which` spells the extension from `PATHEXT` (`claude.CMD`). The helper compares stored and re-resolved paths the same way, so a case-only difference is no change and no warning.
|
||||
- Flagged for review:
|
||||
- `snapshot_survivor` compares names exactly (both sides come from the same Toolhelp32 snapshot API, so they agree for one process); a POSIX `/proc` table gives `comm`, which a process can change itself (a `prctl` rename would count as "another name", but only with an unreadable creation time, which POSIX gives only without `/proc`, where the table is empty and the rule does not apply).
|
||||
- `rerun_binary` treats a missing `binary_resolved_by` (no registry written before Phase 1's field existed is expected) like a pin: kept, no warning.
|
||||
- `init` warnings are not written to the registry; only the call that changed the binary reports it.
|
||||
- The stub's `gate` waits at most 120 s and then goes on; a test that never opens its gate gets a late `ok` instead of a hang.
|
||||
- Left for later: SKILL.md text for N3 (re-run re-resolves unless pinned), N4 (`parent_inactive`) and the `warnings` key on `init` (block 3.5, digest §7.1 items 3 and 7 already name them); the full suite (3.8).
|
||||
- Expected broken: nothing known. The full suite was not run (it runs once in 3.8); `CliTests`, `GateTests`, `TickTests`, `UsageTests` and the rest of `LaunchTests` were not run in this block (a grep of the tests' key-set assertions, `assertEqual(set(…), …)`, finds none on `init` output, so the new `warnings` key breaks no exact-shape check; the new job-entry key `rebase_inactive_parent` is not in `LIST_JOB_KEYS` or the result).
|
||||
- Commits: `62eef3b` (code and tests), then this note's commit.
|
||||
|
||||
Reference in New Issue
Block a user