### Task 8 (Quick win): Acceptance-gate process-tree kill

**Root cause (verified):** `acceptance.py:102-116` spawns the test command via `create_subprocess_shell` (a `/bin/sh -c` wrapper) and on timeout calls `proc.kill()` — which signals only the shell, orphaning a hung `pytest` that keeps holding the worktree's files. The docstring at `acceptance.py:96-99` already *claims* it "kills the process tree"; the code doesn't.

**Files:**
- Modify: `claw_forge/orchestrator/acceptance.py:91-120`
- Test: `tests/orchestrator/test_acceptance_killpg.py` (create)

- [ ] **Step 1: Write the failing test**

```python
# tests/orchestrator/test_acceptance_killpg.py
import time
import pytest
from pathlib import Path
from claw_forge.orchestrator.acceptance import run_test_command


@pytest.mark.asyncio
async def test_timeout_kills_child_process_tree(tmp_path: Path):
    # A shell that spawns a long-lived child; on timeout the whole group
    # must die. We assert the call returns the timeout sentinel quickly.
    start = time.monotonic()
    exit_code, out = await run_test_command(
        tmp_path, "sleep 30 & wait", timeout=1.0,
    )
    elapsed = time.monotonic() - start
    assert elapsed < 5.0  # did not wait for the 30s child
    assert "timed out" in out
```

- [ ] **Step 2: Run test to verify it fails or is flaky**

Run: `uv run pytest tests/orchestrator/test_acceptance_killpg.py -v`
Expected: FAIL/hang — the orphaned `sleep` isn't reaped via the shell-only `kill`.

- [ ] **Step 3: Use a new session + `killpg`**

In `claw_forge/orchestrator/acceptance.py`, add `import os` and `import signal` at top if absent, then:

```python
        proc = await asyncio.create_subprocess_shell(
            command,
            cwd=str(cwd),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT,
            start_new_session=True,  # own process group → killpg reaches children
        )
```

and in the timeout handler:

```python
    except TimeoutError:
        with contextlib.suppress(ProcessLookupError, OSError):
            os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
        with contextlib.suppress(Exception):
            await proc.wait()  # reap
        return (_TIMEOUT_EXIT, f"acceptance tests timed out after {timeout}s")
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `uv run pytest tests/orchestrator/test_acceptance_killpg.py tests/orchestrator/ -q`
Expected: PASS.

- [ ] **Step 5: Lint + commit**

Run: `uv run ruff check claw_forge/ tests/`
```bash
git add claw_forge/orchestrator/acceptance.py tests/orchestrator/test_acceptance_killpg.py
git commit -m "fix(acceptance): kill the whole process group on gate timeout (reap orphaned pytest)"
```

---

## Phase B — Security-layer restoration

> **Sequencing:** Task 9 MUST land before Task 10. Flipping `permission_mode` off `bypassPermissions` while `ALWAYS_BLOCK` still substring-matches would deny `git add` (contains `dd`? no — but `su` matches `subdir`, `mkdir subdir`, `sudo`-free `su`-containing words) on nearly every task.

