---
name: audit-runtime
description: Audit a codebase's source for non-functional violations — how it *runs* rather than whether it is correct. Catches resource leaks (processes, handles, locks outliving their owner; unbounded growth), structural performance smells (blocking the hot path, quadratic or repeated work, polling where an event belongs), and concurrency hazards (shared state mutated without a lock, check-then-act races, locks held across slow work). Use when asked to audit performance, resource use, cleanup, leaks, concurrency, contention, or how the system behaves under load and at scale.
allowed-tools: Read, Grep, Glob, Bash
context: fork
---

Audit the project's source for non-functional violations — the trouble that never shows on the happy path and gets worse exactly as the system scales: leaks, slowness, and contention. This is a read-only audit: report findings, do not fix.

This is the structural complement to the correctness audits. It reads the *source* for patterns that leak, slow, or contend; it is not a profiler and does not measure a running process. A finding is a pattern in the code that will cost a resource, add latency, or race under parallelism — judged from the code, the way the other audits judge invariants from the code.

## Scope

Audit the project's source modules. Ignore tests, plan/spec inputs, and generated files. When auditing a change rather than the whole tree, run `git diff --name-only main...HEAD` first and focus on the touched modules — but still flag any non-functional hazard you see.

This is read-only. Use Bash only for `grep`/`git diff`/file reads. Never run a command that writes, never edit a file.

## Checklist

Work through every category — do not stop early. Each line is one heuristic grep plus a read to confirm. Treat the project CLAUDE.md (and the docs it imports) as the source for which concurrency model, hot paths, and lifecycle boundaries the project actually has, so you judge each hazard against how this system really runs.

**Resource & cleanup — what outlives its owner**

- [ ] 1. Process leak: a spawned subprocess/child process whose teardown is not guaranteed on *every* exit path (normal return, exception, timeout, cancellation, loop/interpreter teardown) — a kill+reap that lives only in one `except` branch, or no `finally` at all
- [ ] 2. Handle leak: a file, socket, connection, lock, temp dir, or async transport opened without a context manager or a guaranteed close on every path
- [ ] 3. Unbounded growth: a cache, list, dict, registry, queue, or log that only ever appends — no eviction, cap, or expiry — so it grows without bound with uptime or scale
- [ ] 4. Interruptible teardown: cleanup that an outer cancel/timeout can interrupt before each resource is released — e.g. resources released in a loop or `gather` with no shielding, so a cancel between iterations strands the rest

**Performance & latency — what costs, and what costs more at scale**

- [ ] 5. Blocking the hot path: synchronous I/O, a blocking subprocess, `time.sleep`, or blocking network in an async path or a latency-sensitive request, where it stalls the event loop or serializes callers
- [ ] 6. Repeated or quadratic work: work recomputed inside a loop that could be hoisted; an O(n²) scan over a set that grows with scale; the same file re-read or re-parsed every iteration
- [ ] 7. Polling for latency: a `sleep`-driven wait that burns wakeups and adds latency proportional to its interval where an event/watcher/notification would serve — judged by cost and responsiveness (the architecture audit judges the same loop against the no-polling *invariant*; here the lens is wasted cycles and added delay)

**Concurrency & contention — what breaks under parallelism**

- [ ] 8. Unguarded shared state: state reachable by parallel workers (a shared file, registry, counter, or in-memory structure) mutated with a read-modify-write that has no lock or atomic operation, so two workers can interleave and lose or corrupt an update
- [ ] 9. Check-then-act race: existence/claim/state checked and then acted on as if unchanged, with a window in which another worker can change it between the check and the act (TOCTOU)
- [ ] 10. Contended critical section: a lock held across a slow or blocking operation (I/O, an await, a subprocess) so every other worker serializes behind it — the lock guards more than the small invariant it needs to

## What to check

For each category, run a heuristic grep, then read the surrounding code to confirm it is a real hazard before reporting it. Defer to the project's described concurrency model and hot paths for what actually runs in parallel and what must stay fast.

1. **Process leak.** Find every place the code spawns a process (`subprocess`, `Popen`, `create_subprocess_*`, `spawn`, `fork`). For each, trace whether the process is killed and reaped on *every* way the spawning scope can exit. A teardown that lives only in a timeout or cancel handler — but not in a `finally` covering an unexpected error or the loop dying underneath it — leaks the process. This is the highest-value check: a leaked engine burns money and corrupts coordination long after its owner is gone.

2. **Handle leak.** Grep for `open(`, socket/connection creation, `Lock()`/`acquire`, `mkdtemp`, and transport/stream creation not wrapped in a `with`/`async with` or a try/finally close. Flag any opened resource without a guaranteed release.

3. **Unbounded growth.** Find collections that are only ever appended to — module-level lists/dicts, caches without a max size, append-only logs or registries. Flag any that grow with uptime or request count and have no eviction, cap, or rotation.

4. **Interruptible teardown.** Read the cleanup paths. Flag teardown that a cancel or timeout can cut short before it finishes — multiple resources released in sequence with no shielding, an `await` inside cleanup that can itself be cancelled — leaving the remaining resources alive.

5. **Blocking the hot path.** Grep for synchronous I/O, `time.sleep`, blocking subprocess, and blocking HTTP in async functions or in a request/response path. Flag every call that stalls the loop or serializes callers. (Where the project also declares non-blocking I/O an *invariant*, the architecture audit will flag the same line against that rule; report it here only for its latency cost and let the consolidation dedupe.)

6. **Repeated or quadratic work.** Read loops for work that does not depend on the loop variable and could be hoisted, for nested scans over a collection that grows with scale, and for the same file/resource read or parsed on every iteration. Flag the ones whose cost grows with input size.

7. **Polling for latency.** Grep for `while True`/`while not` with a `sleep` inside. Flag a poll that waits for state by re-checking on an interval where an event or watcher would respond immediately — the cost is wasted wakeups and latency up to the interval. (A one-shot backoff/delay is fine.)

8. **Unguarded shared state.** Identify state two workers can reach at once — files written by multiple processes, a shared registry or counter, mutable module state under threads/async tasks. Flag a read-modify-write on it with no lock, flock, or atomic operation: two workers interleaving will lose an update or corrupt the state.

9. **Check-then-act race.** Grep for `exists(`, membership/claim checks, and state reads immediately followed by an action that assumes the checked condition still holds. Flag any with a window where a concurrent worker can invalidate the check before the act — prefer an atomic operation or a lock spanning both.

10. **Contended critical section.** Read each lock/flock/critical section. Flag one that holds the lock across a slow or blocking operation — file I/O, a network call, an `await`, a subprocess — when only a small in-memory update needs protecting. The wide lock serializes every other worker behind the slow work.

## Output

Report findings grouped by category, highest severity first. For each:

```
[SEVERITY] <hazard name> — file:line
  What: <the hazardous code, one line>
  Why:  <how it leaks, slows, or contends — and when it bites>
  Fix:  <the concrete change, e.g. "kill+reap the group in a finally covering every exit">
```

Severity: **HIGH** = leaks or corrupts under normal operation (a process leaked on a common exit path, a shared-state race that corrupts coordination, growth that exhausts a resource in ordinary use). **MEDIUM** = degrades at scale (contention, quadratic work, a handle leaked only on an error path, polling that wastes cycles). **LOW** = costs only at extreme scale, or arguable.

End with a one-line verdict: `CLEAN` if no HIGH/MEDIUM findings, otherwise a count per severity. If you read a heuristic hit and decide it is *not* a hazard, do not list it.
