<!-- Python backend verification is ENABLED for this project. The stop hook
     enforces a python cycle in addition to the browser cycle whenever an
     edited file matches `python.verifyPatterns`. -->

## ⚠️ CRITICAL: when NOT to use python-devtools

**`python-devtools` is ONLY for Python backends.** It attaches to a running Python process via **debugpy** (Debug Adapter Protocol — already-listening / PID injection / process name / Docker container). It does **NOT** work with any other runtime. Do **NOT** call `MCP:pdt_*` tools for projects whose backend is:
- Node.js (Express, Next, Nest, …) — use the **node cycle** (`ironbee node enable`, `MCP:ndt_*` tools) instead
- Java (Spring, Quarkus, Micronaut, …) — use the JVM debugger, not supported by IronBee yet
- Go, Rust, Ruby, .NET, PHP, Elixir, Kotlin/JVM, Swift — not supported yet

**How to tell whether the backend is Python:**
- `requirements.txt`, `pyproject.toml`, `Pipfile`, or `setup.py` is the main project manifest.
- The dev server command is `python manage.py runserver`, `flask run`, `uvicorn`/`gunicorn`, `python app.py`, `celery worker`, etc.

If you see `package.json` (with no Python manifest), `pom.xml`, `build.gradle`, `go.mod`, `Cargo.toml`, `Gemfile`, `composer.json`, `*.csproj` — the backend is NOT Python. Do NOT call any `MCP:pdt_*` tools.

**Runtime prerequisite: `debugpy` must be installed in the TARGET's Python environment** (`pip install debugpy`). It is a third-party package, not built into the interpreter. If `MCP:pdt_debug_connect` fails because debugpy is missing, report that clearly (and how to install it) — do NOT fake evidence.

**Misconfiguration recovery.** If you read this section it means the operator enabled the python cycle for this project. If the backend isn't actually Python, this was a mistake — the stop hook will keep blocking the gate (with `incomplete_tools` for the python cycle) until you call `MCP:pdt_debug_connect` (which will fail) or `maxRetries` is exhausted. Don't attempt the connection. Stop and tell the user clearly: the project's backend is not Python; ask them to run `ironbee python disable` to unblock the gate. Continue with browser-cycle verification only in the meantime.

## Python flow

1. **Identify the running Python process** — note its PID, container name (`docker compose ps`), or debugpy listen port (default 5678; the target may have been started with `python -m debugpy --listen 127.0.0.1:5678 app.py` or call `debugpy.listen(...)` itself).
2. **Connect**: `MCP:pdt_debug_connect` with one of `host`+`debugpyPort` (already-listening) / `pid` (debugpy is injected into the running process; POSIX) / `processName` / `containerId` / `containerName`. Probes are non-blocking — the target keeps running.
3. **Pick an evidence path** per changed code path:
   - **Probe path** (proves the code path executed):
     - Set a probe at the changed location: `MCP:pdt_debug_put-tracepoint` (checkpoint), `MCP:pdt_debug_put-logpoint` (logged expression), or `MCP:pdt_debug_put-exceptionpoint` (raised/uncaught exception).
     - **Exercise the path** (e.g. send a request from the browser, run a CLI command, post to a queue) — without this the probe never fires.
     - Read collected snapshots: `MCP:pdt_debug_get-probe-snapshots`. At least one probe must come back with `triggered: true`.
   - **Log path** (proves no errors during execution):
     - Exercise the path.
     - Read errors: `MCP:pdt_debug_get-logs` with the error-level filter (the target's stdout/stderr, captured via DAP output redirect).
   - **Network path** (for egress / API-integration changes — proves the expected OUTBOUND call happened):
     - Call `MCP:pdt_o11y_get-http-requests` once to install the in-process capture agent (forward-looking — only traffic AFTER this call is captured; covers `urllib`/`http.client`, `requests`, `httpx`, `aiohttp`; NOT pycurl / grpcio C-core / raw sockets).
     - **Exercise the path**, then read `MCP:pdt_o11y_get-http-requests` again and confirm the expected request(s) / response status.
   - **Thread-dump path** (for deadlock / GIL-contention / hang fixes):
     - `MCP:pdt_debug_dump-threads` snapshots every thread's stack at once (py-spy-style; momentarily pauses, then resumes). Confirm the expected thread states (e.g. the previously-deadlocked thread is no longer blocked).
4. **Disconnect** (optional): `MCP:pdt_debug_disconnect` (the target keeps running).

**Batch (speed):** connect (step 2) is standalone discovery. Batch consecutive `pdt_*` calls in one `MCP:execute` batch (the compose server's single GLOBAL batch tool; there is no `pdt_execute`) — set several probes together, then later read snapshots/logs together. The exercise step is ALWAYS separate: whatever triggers the code path (a browser/backend call on another server, a CLI command, the user) can't share the batch — so python runs as set probes (batch) → exercise (separate) → read snapshots (batch).

### Verdict fields
The verdict is platform-agnostic — you submit only semantic judgment:

```json
{
  "session_id": "<sid>",
  "status": "pass",
  "checks": ["POST /api/orders returned 201", "tracepoint at views.py:42 fired once"]
}
```

Python-cycle pass criteria:
- If probes were set, at least one must have triggered (proves the code path executed).
- If only logs were used, no ERROR-level entries.
- If the network path was used, the expected outbound request(s) must appear with the expected status.
- If multiple forms were used, all their conditions must hold.

## Multi-cycle (browser + python simultaneously)

Common case: in the same task you edit a `.tsx` component (browser-cycle) and an `api/*.py` handler (python-cycle). Both cycles activate. **Single** `verification-start`, **single** `verdict.json`, **single** retry counter cover both. The verdict shape doesn't change — one verdict regardless of how many cycles ran:

```json
{
  "session_id": "<sid>",
  "status": "pass",
  "checks": ["checkout submits", "POST /api/orders returned 201", "no console errors"]
}
```

For a multi-cycle `pass`, BOTH cycles' pass criteria must hold.
