# Build & Debug Loop

Protocol for Step 3.5 in `/tas-dev`. Runs in **main session** (required for MCP browser tools).

## Stack Commands

| Stack | Start command | Ready signal | Browser check |
|---|---|---|---|
| `.NET` | `dotnet run` | `Now listening on:` | No |
| `Next.js` | `npm run dev` | `Ready in` / `✓ compiled` | Yes |
| `NestJS` | `npm run start:dev` | `Nest application successfully started` | No (API only) |
| `Astro` | `npm run dev` | `astro  v` + port line | Yes |
| `ReactJS` | `npm run dev` OR `npm start` | `compiled successfully` / `Local:` | Yes |
| `React Native` | `npx expo start` | `Metro waiting on` | No (native) |
| `Python FastAPI` | `uvicorn main:app --reload` | `Application startup complete` | No (API only) |
| `Python Django` | `python manage.py runserver` | `Starting development server at` | No (API only) |
| `Python Flask` | `flask run` OR `python app.py` | `Running on http://` | No (API only) |
| `Node.js` | `npm run start` OR `node src/index.js` | Port listen line OR exit code 0 | No |

**Port detection**: extract port from ready signal line. Default fallbacks: .NET→5000, Next.js→3000, NestJS→3000, Astro→4321, React→3000, Python→8000, Flask→5000, Node→3000.

**Working dir**: use path from Feature Technical Plan's stack section. If absent, infer from `package.json` / `*.csproj` / `*.py` location relative to changed files.

## Loop Protocol

```
max_attempts = tas.yaml[build_debug_attempts] ?? 3
attempt = 0
server_pid = null
last_error_sig = null   # normalized signature of the previous attempt's failure

PRE-FLIGHT (before loop): run Pre-flight Cleanup (see section below).

try:
  while attempt < max_attempts:
    1. Start server (background process, timeout 60s waiting for ready signal)
       → capture PID returned by Bash run_in_background → server_pid = PID
    2. Capture stdout + stderr for 30s after ready (or until crash)
    3. Evaluate result:
       a. Server crashed (exit code != 0 OR no ready signal in 60s)
          → extract error lines → compute error_sig (see No-divergence guard)
          → NO-DIVERGENCE GUARD: if error_sig == last_error_sig → ABORT loop now
            (the last fix did not change the failure — retrying wastes attempts).
            Go to warning block with reason "fix ineffective — identical failure".
          → else: last_error_sig = error_sig → call build-resolver with error context
          → apply returned fix → kill_server(server_pid) → attempt++ → retry
       b. Server up but error patterns in log (see Error Patterns below)
          → same as (a), including the no-divergence guard
       c. Server up, no errors → proceed to Browser Check (if applicable)
          → if browser check passes → kill_server(server_pid) → PASS, exit loop
          → if browser check fails → call build-resolver with browser error context
            → apply fix → kill_server(server_pid) → attempt++ → retry
    4. After applying fix: tick attempted action in log

  if attempt >= max_attempts:
    STOP. Output warning block (see Warning Format below).
    Do NOT proceed to Step 4 review.

# No-divergence guard: error_sig = normalize(error) — lowercase the top exception
# class/message + the first in-repo file:line from the trace, strip timestamps/PIDs/
# memory addresses. Two attempts with the same error_sig mean the applied fix had no
# effect on the failure; abort immediately instead of burning the remaining attempts.

finally:
  TEAR DOWN (always): kill_server(server_pid) if alive. See Tear Down section.
```

**MANDATORY**: kill_server() must run on every exit path — PASS, FAIL, max_attempts, exception, user interrupt. Orphan dev servers cause RAM exhaustion, port conflicts, and `.next`/`node_modules/.cache` corruption on next run.

## Pre-flight Cleanup

Run **before** loop starts. Kills orphan dev servers from prior runs/crashes that hold target port or pile up RAM.

### Windows (PowerShell)
```powershell
# Kill process holding target port (if any)
$port = {target_port}
$conn = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
if ($conn) {
  $procId = $conn.OwningProcess
  $proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
  if ($proc -and $proc.ProcessName -match '^(node|dotnet|python|npm|bun|deno|pnpm|yarn)$') {
    Stop-Process -Id $procId -Force
    Write-Host "Killed stale $($proc.ProcessName) PID=$procId on port $port"
  }
}
```

### Unix (bash)
```bash
port={target_port}
pid=$(lsof -ti :$port 2>/dev/null)
if [ -n "$pid" ]; then
  pname=$(ps -p $pid -o comm= 2>/dev/null)
  case "$pname" in
    node|dotnet|python|python3|npm|bun|deno|pnpm|yarn) kill -9 $pid; echo "Killed stale $pname PID=$pid on port $port" ;;
  esac
fi
```

**Safety**: only kill if process name matches the dev-server pattern (`node`/`dotnet`/`python`/`npm`/`bun`/`deno`/`pnpm`/`yarn`). Skip kill if foreign process — surface port conflict to user instead.

## Tear Down

Run on **every** loop exit path (success, failure, exception, interrupt).

> `$serverPid`, not `$pid` — `$PID` is a PowerShell automatic variable (the current shell's process id); assigning to it is wrong and case-insensitively collides.

### Windows
```powershell
$serverPid = {server_pid}
if ($serverPid) {
  # Graceful: kill process tree (dev servers spawn child workers)
  taskkill /PID $serverPid /T 2>$null
  Start-Sleep -Seconds 3
  # Force if still alive
  if (Get-Process -Id $serverPid -ErrorAction SilentlyContinue) {
    taskkill /PID $serverPid /T /F 2>$null
  }
}
# Belt-and-suspenders: a forked child may have re-bound the port under a new PID.
# Re-run the Pre-flight port sweep on {target_port} to catch it.
```

### Unix
```bash
serverPid={server_pid}
if [ -n "$serverPid" ] && kill -0 $serverPid 2>/dev/null; then
  # SIGTERM tree (negative PID = process group)
  kill -TERM -$serverPid 2>/dev/null || kill -TERM $serverPid
  sleep 3
  # SIGKILL if still alive
  kill -0 $serverPid 2>/dev/null && (kill -KILL -$serverPid 2>/dev/null || kill -KILL $serverPid)
fi
# Belt-and-suspenders: re-run the Pre-flight port sweep on $port to catch a
# detached child that re-bound the port under a new PID.
```

**Why graceful first**: SIGTERM lets Next.js/Vite finish writing `.next`/`.vite` cache. SIGKILL mid-write corrupts cache → next start fails with ENOENT/SyntaxError on chunk files.

**Multi-stack runs**: tear down current stack's server BEFORE starting next stack. Never overlap.

## Error Patterns (log scan)

Match any of these as "error in log" even when exit code = 0:

- `.NET`: `Unhandled exception`, `Application is shutting down`, `fail:`
- `Node/TS`: `Error:`, `UnhandledPromiseRejection`, `Cannot find module`, `SyntaxError`
- `NestJS`: `[ExceptionHandler]`, `Nest can't resolve dependencies`
- `Python`: `Traceback (most recent call last)`, `ModuleNotFoundError`, `ImportError`
- `React/Next`: `Failed to compile`, `Module not found`, `error TS`
- `Astro`: `[ERROR]`, `Build failed`

## Browser Check (frontend stacks only)

Token-cheap path: **single curl snapshot + Grep presence test**. No MCP browser, no screenshot, no headless render.

### Single-snapshot rule

Fetch each URL **once per session**. Cache to `.tas/tmp/snap-{slug}.html`. Re-Grep the cached file for every subsequent check. **Re-fetch trigger** (concrete): only when a file under the stack's source dir has an mtime newer than `.tas/tmp/snap-{slug}.html` — i.e. a build-resolver fix was applied since the snapshot. No source change since snapshot → re-Grep the cache, never re-fetch.

### 1. HTTP status probe (cheapest)
```bash
curl -sI http://localhost:{port}{path}
```
FAIL if status ≥ 500, or status ≠ expected from AC.

### 2. HTML snapshot
```bash
mkdir -p .tas/tmp
curl -s -o .tas/tmp/snap-{slug}.html -w "%{http_code}\n" http://localhost:{port}{path}
```
FAIL if exit ≠ 0 or file size < 200 bytes (likely blank/error page).

### 3. Section-presence Grep (no re-fetch)
For each expected element from AC "Then" clause:
```
Grep -F "{expected marker}" .tas/tmp/snap-{slug}.html
```
Markers: heading text, element id, data-testid attr, route-specific class. FAIL if any required marker missing.

### 4. Error-overlay Grep
```
Grep -i -E "(unhandled|exception|stack trace|error overlay|hydration failed)" .tas/tmp/snap-{slug}.html
```
FAIL if any match (inline error page rendered).

### 5. AC-based API verification
```
For each AC with HTTP verb + path (GET/POST/PUT/DELETE /path):
  curl -s -w "\n%{http_code}" http://localhost:{port}{path}
  check status matches expected
  Grep response body for fields from AC "Then" clause
```
Skip if no HTTP endpoint patterns in ACs.

## Context to pass build-resolver agent

```
Stack: {stack}
Command run: {command}
Attempt: {N} of {max}
Exit code: {code}
Error output:
{error neighborhood — ≤20 lines (head+tail of the last 50 stdout+stderr), per build-resolver's input cap. Do NOT dump the full 50.}
Browser errors (if any):
{console errors list}
AC being verified (if functional check):
{AC text}
Feature scope files: {list of files changed in this stack}
```

## Warning Format (max attempts exceeded)

```
⚠️ Build debug limit reached ({N}/{max} attempts) — manual intervention required.

Stack: {stack}
Command: {command}

Issues found:
{numbered list of distinct errors across all attempts}

Actions taken:
{numbered list of fixes attempted, each with: attempt N → fix description}

Last build-resolver diagnosis:
{paste diagnosis from final attempt}

Predicted root cause:
{build-resolver's last "Root cause" field}

Recommended next step:
{build-resolver's "Fix" field from last attempt}
```

## Logging

After loop completes (pass or fail), append to Feature Technical file `## Build Debug Log` section (create if absent):

```markdown
## Build Debug Log

### {datetime} — {stack}
- Result: PASS / FAIL (attempt {N}/{max})
- Command: {command}
- Attempts: {list each attempt + outcome}
- Fixes applied: {list}
```
