Orchestrata Ultimate › Plans › briefs › qwen › Q8-report

Q8 report — hooks that judge a process by the words in its command line

Source: plans/briefs/qwen/Q8-report.md at 8a2cb42ad · last changed 2026-10-12 · first committed 2026-10-12 · 431 lines.

Prose pending.

Plan text

Q8 report — hooks that judge a process by the words in its command line

Worktree /home/mohamed/workspace/wt-HOOK-MATCH, branch fix/hook-process-matching, HEAD 762935ab6. Nothing committed; changes staged, scratch (.q8/) unstaged.

This report covers both Q8-hook-process-matching.md and Q8-continuation.md. The Qwen executor's session ended on the dispatcher timeout before writing this report; the continuation §2 work (the leaking live_run_tree) had not been done at all, so it was done here, red first. Deviations found in the inherited work are listed under "Deviations".

1. Every copy and registration of the reaper

git ls-files | grep -i reap-orphan:

.claude/hooks/reap-orphan-waiters.sh
plugins/orchestrata/project-hooks/reap-orphan-waiters.sh

grep -rn reap-orphan-waiters .claude/settings*.json claude/ plugins/:

.claude/settings.json:46:  "command": "cd \"$CLAUDE_PROJECT_DIR\" && bash .claude/hooks/reap-orphan-waiters.sh"
plugins/orchestrata/hooks/hooks.json:170:  "command": "bash \"${CLAUDE_PLUGIN_ROOT}\"/project-hooks/_dispatch.sh reap-orphan-waiters.sh"

Two tracked copies, two registrations (SessionStart in both). The plugins copy is generated by claude/scripts/gen_plugin.py; it was regenerated, never hand-edited:

$ .venv/bin/python claude/scripts/gen_plugin.py
gen-plugin: hooks: 58 file(s) (0 written, 0 removed)
...
gen rc=0
$ .venv/bin/python claude/scripts/gen_plugin.py --check
gen-plugin: plugin is current under /home/mohamed/workspace/wt-HOOK-MATCH
check rc=0

diff between each source and its generated copy: empty (REAPER-COPIES-IDENTICAL, CONTEXT-COPIES-IDENTICAL).

2. Part 1 (reaper) — requirement to implementing line

All line numbers in .claude/hooks/reap-orphan-waiters.sh (identical in the generated copy):

3. Part 2 (status hook) — requirement to implementing line

In claude/hooks/test_run_context.py (identical in plugins/orchestrata/hooks/test_run_context.py):

4. Tests — red first, then green

Red evidence (pre-fix code), files in .q8/

Green evidence (post-fix)

5. The replaced test (T2 authorisation)

test_a_matching_waiter_past_the_age_gate_is_reaped at HEAD asserted the defect — a waiter whose parent (the live test process) is alive is killed anyway, and the reaper ran unscoped over the whole host:

result = _run(min_age_min="0")
assert result.returncode == 0

deadline = time.time() + 15
while time.time() < deadline and proc.poll() is None:
    time.sleep(0.5)
assert proc.poll() is not None, (
    "the reaper left an orphaned waiter alive past its age gate; "
    f"stdout={result.stdout!r}"

Authorising paragraph (brief §2, Tests):

T2 replaces test_a_matching_waiter_past_the_age_gate_is_reaped. That test asserts the defect of §1.1 (a waiter with a live parent is killed), against the hook's own header. The orchestrator authorises this one contract change. In the report, quote the old test's assertion and this paragraph.

6. T7 — the four pre-existing tests

An AST-level comparison of the assertion expressions, HEAD vs working tree:

test_the_reaper_never_gates_the_session
  HEAD: assert _run().returncode == 0
  new:  assert _run(str(sleeper.pid)).returncode == 0
test_a_young_waiter_is_left_alone
  HEAD: assert proc.poll() is None, 'fixture loop exited on its own' / 'the reaper killed a young waiter'
  new:  assert _alive(pid),        'fixture loop exited on its own' / 'the reaper killed a young waiter'
test_a_loop_without_sleep_or_empty_body_is_not_matched
  HEAD: assert proc.poll() is None, 'the reaper killed a working while-read loop'
  new:  assert _alive(pid),         'the reaper killed a working while-read loop'
test_hook_is_registered_at_session_start
  UNCHANGED

The only expression changes are the ones T7 itself mandates: every reaper invocation now passes ONLY_PIDS (naming the fixture pid the test started), and each fixture became a helper-adopted orphan, so the process handle (proc.poll()) became a /proc liveness read (_alive(pid)) on the adopted pid. Assertion messages and the gates they name (session-exit, age, shape, registration) are identical; each fixture fails the gate it names, not the orphan check (adopter set names the helper). Full diff: git diff HEAD -- tests/scripts/test_reap_orphan_waiters.py.

7. Continuation §2 — the leaking live_run_tree

8. Verification 1 — the forced run

Targets (.q8/q8-targets.txt, 97 modules): the four brief-named modules plus the affected_tests.py set. The affected-set computation:

$ env -u PYTHONPATH .venv/bin/python scripts/affected_tests.py --json --files <six changed files>
"escalate": false
"tests": ["tests/claude", "tests/claude/test_run_detection_ignores_watchers.py",
          "tests/claude/test_test_run_context.py", "tests/claude/test_test_run_context_coverage.py",
          "tests/infrastructure", "tests/scripts/test_reap_orphan_waiters.py",
          "tests/scripts/test_session_start_hooks.py"]
"untested": []

escalate is false, so no whole-suite STOP. Directory entries (tests/claude, tests/infrastructure) were expanded to their 95 test_*.py modules; no tests/conftest.py entry was present.

The run (detached, rule 9/10 form env -u PYTHONPATH -u PYTEST_ADDOPTS bash .run_exclusive.sh --force --json <97 targets>):

test_totals: {'passed': 1545, 'failed': 0, 'errors': 0, 'skipped': 0}
reused: {'modules': 0, 'tests': 0}
run rc=0   time_seconds: 67.1   head_commit: 762935ab6176b3231bf49a413850e52d4e995760

0 failed, 0 errors, 0 skipped, reused 0, rc 0.

9. Verification 2 — host-safety proof

Immediately before the run:

$ pgrep -fa '(while|until)[[:space:]]' > .q8/loops-before.txt
$ wc -l < .q8/loops-before.txt
0

Immediately after:

$ pgrep -fa '(while|until)[[:space:]]' > .q8/loops-after.txt
$ wc -l < .q8/loops-after.txt
0

Result: the before-file lists zero pids, so zero foreign pids could disappear, and none did; the after-file is empty, so none of the (while|until) fixture loops my tests started (T2–T4, T6, Verification 3) leaked past the run. (The orchestrator's waiters the brief expects in the list were already gone: the dispatcher's 90-minute timeout ended that session before this executor started.)

10. Verification 3 — the incident, end to end

bash /tmp/opencode/q8-v3-repro.sh (scoped: ORPHAN_WAITER_MIN_AGE_MIN=0 ORPHAN_WAITER_ONLY_PIDS="<both pids>"), full output saved to .q8/v3-repro.txt:

DISPATCH=2000231 MID=2000232 CHILD=2000234
--- fixtures before the reaper ---
    PID    PPID COMMAND
2000231 2000230 .venv/bin/python -c import time; time.sleep(120) timeout 3600 bash -c 'while pgrep -f "scripts/run_tests[.]py" >/dev/null; d
2000232 2000230 bash -c bash -c "until test -f /nonexistent-q8-incident-repro; do sleep 5; done" & echo $! > /tmp/opencode/q8-v3-child; wait
2000234 2000232 bash -c until test -f /nonexistent-q8-incident-repro; do sleep 5; done
--- ORPHAN_WAITER_MIN_AGE_MIN=0 ORPHAN_WAITER_ONLY_PIDS="2000231 2000234" bash .claude/hooks/reap-orphan-waiters.sh ---
hook rc=0
--- both alive after the reaper? ---
    PID    PPID STAT ELAPSED COMMAND
2000231 2000230 S          1 .venv/bin/python -c import time; time.sleep(120) timeout 3600 bash -c 'while pgrep -f "scripts/run_tests[.]py" >/dev/null; do sleep 15; d
2000232 2000230 S          1 bash -c bash -c "until test -f /nonexistent-q8-incident-repro; do sleep 5; done" & echo $! > /tmp/opencode/q8-v3-child; wait
2000234 2000232 S          1 bash -c until test -f /nonexistent-q8-incident-repro; do sleep 5; done
ps rc=0 (0 = all three listed = alive)
cleanup kill rc=0
all fixtures gone after cleanup

The reaper printed no killed line: the non-shell quoting a loop (the 930286 shape) and the waiter with a live parent (the 931060/931062 shape) both survived, and the fixtures were then killed by the script itself.

11. Orphan check (continuation §3)

After the forced run:

$ for p in $(pgrep -f -- '-m pytest'); do [ "$(awk '{print $4}' /proc/$p/stat 2>/dev/null)" = 1 ] && readlink /proc/$p/cwd; done | grep -c wt-HOOK-MATCH
0

Zero -m pytest orphans with ppid 1 and this worktree as cwd. (grep -c exits 1 on a zero count; the 0 is the pass.)

12. Verification 4 — lint, gates, staging

bash -n both hook copies:        rc 0
shellcheck both hook copies:     rc 0
ruff check (4 Python files):     All checks passed!
black --check (4 Python files):  4 files would be left unchanged
pyright (4 Python files):        0 errors, 0 warnings, 0 informations
check-deps --include-tests:      rc 0 — "✅ All third-party imports are declared in pyproject.toml."
gen_plugin.py:                   0 written; --check rc 0 ("plugin is current")

gate.py run needed one environment repair first, documented under Deviations (D4). After it:

gate: PASS
  - 5 rule clause(s), no violations
  fact base: 426,459 facts across 23 predicates
gate rc=0

Staged: the six changed files (both hook copies, both test_run_context.py copies, both test modules), the two Q8 brief files, and this report. Never staged: .q8/, reports/ (reports/run-tests-latest.json was touched by the test runs and stays unstaged), plans/ast-*.json (see D4 — modified by the refresh, deliberately unstaged), .hostlock.sh, .run_exclusive.sh.

scripts/gate.py review-staged: see the final message / staged diff.

13. Deviations found in the inherited (Qwen) work, and what was done

14. BLOCKED items

None.