# BUGSWEEPER Agent Usage Guide

BUGSWEEPER is a live debugging framework embedded in the WITwhat app. It exposes an HTTP API on `127.0.0.1:9876` that gives agents full programmatic control over the running Slint frontend — UI state, data layer, callbacks, and more.

**Feature-gated:** Production builds are unaffected. Only builds with `--features bugsweeper` include the server.

## Building and Running

```bash
# Build with BUGSWEEPER enabled
cargo build --features bugsweeper

# Run the app in the background — server starts automatically on 127.0.0.1:9876
./target/debug/app.exe &

# Override the default port via environment variable
BUGSWEEPER_PORT=9999 ./target/debug/app.exe &
```

### Waiting for the App to Be Ready

**Do NOT sleep for a fixed duration after launching the app.** Instead, poll the health endpoint until it responds. The app typically loads in 1-2 seconds — a fixed 8-second sleep wastes tokens and time.

```bash
# Poll health endpoint — exits as soon as the server is up (checks every 0.5s, 30s timeout)
for i in $(seq 1 60); do curl -s http://127.0.0.1:9876/api/debug/health && break; sleep 0.5; done
```

Only proceed with debug commands after this poll succeeds.

## Session Discipline

**Any mutations to card data, properties, or state performed during a debugging session MUST be reverted before the session ends.** This includes:

- Properties changed via `PUT /api/ui/property/{name}` (e.g. search-text, active-mode-index)
- Callbacks invoked that alter persistent state (e.g. card-save-note, card-archive)
- Discovery mode or filter changes

**Recommended pattern:** Snapshot state before testing, restore after.

```bash
# Save current state
ORIGINAL_MODE=$(curl -s http://127.0.0.1:9876/api/ui/property/active-mode-index)
ORIGINAL_SEARCH=$(curl -s http://127.0.0.1:9876/api/ui/property/search-text)

# ... perform your testing ...

# Restore original state
curl -s -X PUT -d "{\"value\":$(echo $ORIGINAL_MODE | grep -o '"value":[^,}]*' | cut -d: -f2)}" http://127.0.0.1:9876/api/ui/property/active-mode-index
curl -s -X PUT -d '{"value":""}' http://127.0.0.1:9876/api/ui/property/search-text
```

**Do not invoke callbacks that write to external services** (e.g. `card-save-rx-od`, `card-save-discord-username`, `settings-save-clicked`) unless you are explicitly testing that write-back path and have a plan to revert.

## Endpoint Reference

### Debug

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/debug/health` | Liveness probe. Returns `{"ok": true}` |
| GET | `/api/debug/endpoints` | Lists all registered routes |

### Discovery

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/ui/registry` | Lists all registered properties and callbacks with type metadata |

```bash
# Discover all available properties and callbacks
curl -s http://127.0.0.1:9876/api/ui/registry

# List just writable properties (using python)
curl -s http://127.0.0.1:9876/api/ui/registry | python -c "
import sys, json
reg = json.load(sys.stdin)
for p in reg['properties']:
    if p['writable']:
        print(f\"{p['name']} ({p['type']})\")
"
```

### UI — Cards

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/ui/cards` | Full card model as JSON array. Every CardData field serialized (except opaque `avatar-image` handle) |

```bash
# Get all cards
curl -s http://127.0.0.1:9876/api/ui/cards

# Count cards (using bash)
curl -s http://127.0.0.1:9876/api/ui/cards | python -c "import sys,json; print(len(json.load(sys.stdin)))"

# Get a specific card's status (by index, 0-based)
curl -s http://127.0.0.1:9876/api/ui/cards | python -c "import sys,json; cards=json.load(sys.stdin); print(json.dumps(cards[0], indent=2))"
```

### UI — GUI Interaction

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/ui/element/{name}` | Get geometry (x, y, width, height) of a named UI element relative to client area |
| POST | `/api/ui/click` | Simulate a mouse left-click at (x, y) client-area coordinates |

**Available element names:** `window`

Elements can be extended incrementally by adding entries to `ELEMENT_NAMES` and the match arm in `get_element_geometry` in `crates/app/src/main.rs`.

```bash
# Get window client-area dimensions
curl -s http://127.0.0.1:9876/api/ui/element/window
# {"name":"window","x":0,"y":0,"width":1200,"height":800}

# List available element names (request an unknown name)
curl -s http://127.0.0.1:9876/api/ui/element/unknown
# {"error":"Unknown element 'unknown'. Available elements: [window]"}

# Click at coordinates (100, 200) relative to client area
curl -s -X POST -d '{"x":100,"y":200}' http://127.0.0.1:9876/api/ui/click
# {"ok":true,"client_x":100,"client_y":200,"screen_x":...,"screen_y":...}
```

### UI — Product Models

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/ui/product-tiles` | Current product tile grid model as JSON array |
| GET | `/api/ui/detail-units` | Current product detail sidecar units model (includes product context) |

```bash
# Get all product tiles currently displayed in the grid
curl -s http://127.0.0.1:9876/api/ui/product-tiles

# Get the detail sidecar state + units list
curl -s http://127.0.0.1:9876/api/ui/detail-units

# Count how many units are loaded in the sidecar
curl -s http://127.0.0.1:9876/api/ui/detail-units | python -c "import sys,json; d=json.load(sys.stdin); print(f'{len(d[\"units\"])} units for {d[\"product_name\"]}')"
```

### UI — Properties

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/ui/property/{name}` | Read a named Slint property |
| PUT | `/api/ui/property/{name}` | Write a named Slint property. Body: `{"value": ...}` |

**Readable properties (23):**
`search-text`, `active-mode-index`, `toast-message`, `toast-visible`, `toast-is-warning`, `search-focused`, `connection-status`, `show-option-grid`, `hidden-archived-count`, `current-show-archived`, `settings-modal-open`, `pending-edit-count`, `lookup-modal-open`, `show-recipient-picker`, `discord-token-configured`, `card-count` (virtual — returns cards model length), `refresh-all-disabled`, `search-result-count`, `add-product-visible`, `add-product-name`, `add-product-shopify-url`, `add-product-validation-error`, `product-detail-visible`

**Writable properties (14):**
`search-text`, `active-mode-index`, `toast-message`, `toast-visible`, `search-focused`, `current-show-archived`, `settings-modal-open`, `lookup-modal-open`, `show-recipient-picker`, `add-product-visible`, `add-product-name`, `add-product-shopify-url`, `add-product-validation-error`, `product-detail-visible`

```bash
# Read the current search text
curl -s http://127.0.0.1:9876/api/ui/property/search-text

# Set search text
curl -s -X PUT -d '{"value":"alice"}' http://127.0.0.1:9876/api/ui/property/search-text

# Check how many cards are visible
curl -s http://127.0.0.1:9876/api/ui/property/card-count

# Check connection status (0=unconfigured, 1=connecting, 2=connected, 3=disconnected, 4=no-shopify)
curl -s http://127.0.0.1:9876/api/ui/property/connection-status
```

### UI — Callbacks

| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/ui/callback` | Invoke a named callback. Body: `{"name": "...", "args": [...]}` |

**Available callbacks (45):**
- **Navigation:** `mode-switch-up`, `mode-switch-down`, `esc-pressed`, `home-pressed`, `end-pressed`, `tab-clicked(int)`, `tile-clicked(string)`, `breadcrumb-back`
- **Search:** `search-changed(string)`, `character-pressed(string)`, `chip-toggled(int)`
- **Refresh:** `refresh-all-clicked`, `card-refresh(int)`
- **Card actions:** `card-summary(int)`, `card-save-note(int, string)`, `card-add-item(int)`, `card-remove-item(int)`, `card-confirm-remove-item(int)`, `card-remove-single-item(int, int)`
- **Archive:** `toggle-show-archived`, `card-archive(int)`, `card-unarchive(int)`, `card-archive-now(int)`, `toast-undo`
- **External:** `card-open-discord-dm(int)`, `card-copy-email(int)`, `card-open-shopify-customer(int)`, `card-open-shopify-order(int)`
- **Editing:** `card-save-rx-od(int, string)`, `card-save-rx-os(int, string)`, `card-save-purpose(int, string)`, `card-copy-rx(int)`, `card-save-discord-username(int, string)`
- **Modal:** `settings-clicked`, `pick-recipient-clicked(int)`, `settings-save-clicked`
- **Lifecycle:** `sync-cards-updated`
- **Products:** `on-product-add-clicked`, `on-add-product-submit`, `on-add-product-discard`, `on-product-sidecar-set-image(string)`, `on-product-sidecar-view-shopify(string)`, `on-product-sidecar-view-issue(string)`, `on-product-sidecar-archive(string)`, `on-product-create-unit-with-serial(string, string)`, `on-product-unit-selection-changed(string, string)`, `on-add-product-name-changed(string)`, `on-add-product-shopify-url-changed(string)`, `on-add-product-suggestion-selected(string)`

```bash
# Switch to "By Recipient" tab (index 2)
curl -s -X POST -d '{"name":"tab-clicked","args":[2]}' http://127.0.0.1:9876/api/ui/callback

# Trigger search
curl -s -X POST -d '{"name":"search-changed","args":["alice"]}' http://127.0.0.1:9876/api/ui/callback

# Open card summary popover for card at index 0
curl -s -X POST -d '{"name":"card-summary","args":[0]}' http://127.0.0.1:9876/api/ui/callback

# Refresh all cards
curl -s -X POST -d '{"name":"refresh-all-clicked","args":[]}' http://127.0.0.1:9876/api/ui/callback

# Press Esc
curl -s -X POST -d '{"name":"esc-pressed","args":[]}' http://127.0.0.1:9876/api/ui/callback
```

### Data — SQL Queries

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/data/query?sql=...` | Execute a read-only SQL query against SQLite |

**SELECT and read-only PRAGMA only.** Queries containing INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, REPLACE, or ATTACH are rejected. Assignment PRAGMAs (containing `=`) are also rejected.

```bash
# List all recipients
curl -s "http://127.0.0.1:9876/api/data/query?sql=SELECT+*+FROM+recipients"

# Count cards by status
curl -s "http://127.0.0.1:9876/api/data/query?sql=SELECT+shipment_status,+COUNT(*)+as+cnt+FROM+cards+GROUP+BY+shipment_status"

# Find a specific recipient
curl -s "http://127.0.0.1:9876/api/data/query?sql=SELECT+*+FROM+recipients+WHERE+recipient_id+LIKE+'%alice%'"

# Check archive records
curl -s "http://127.0.0.1:9876/api/data/query?sql=SELECT+*+FROM+archive_records"

# Inspect table schema
curl -s "http://127.0.0.1:9876/api/data/query?sql=PRAGMA+table_info(cards)"

# Check database integrity
curl -s "http://127.0.0.1:9876/api/data/query?sql=PRAGMA+integrity_check"

# List indexes on a table
curl -s "http://127.0.0.1:9876/api/data/query?sql=PRAGMA+index_list(cards)"
```

### State

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/state/discovery` | Current discovery mode, search text, filters, show-archived flag |
| GET | `/api/state/archive` | Archive state for all cards |

```bash
# Get current discovery state
curl -s http://127.0.0.1:9876/api/state/discovery

# Get archive state
curl -s http://127.0.0.1:9876/api/state/archive
```

### App

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/app/config` | Non-secret app configuration (has_X booleans, not actual tokens) |
| GET | `/api/app/window` | Window title, dimensions, visibility |
| POST | `/api/app/quit` | Gracefully close the application |

```bash
# Check if Shopify/Discord are configured
curl -s http://127.0.0.1:9876/api/app/config

# Get window info
curl -s http://127.0.0.1:9876/api/app/window
```

## Typical Agent Workflows

### Pre-UAT Smoke Test

```bash
# 1. Wait for app to be ready (do NOT use a fixed sleep)
for i in $(seq 1 60); do curl -s http://127.0.0.1:9876/api/debug/health && break; sleep 0.5; done

# 2. Check connection status
curl -s http://127.0.0.1:9876/api/ui/property/connection-status

# 3. Verify cards loaded
curl -s http://127.0.0.1:9876/api/ui/property/card-count

# 4. Dump full card model for inspection
curl -s http://127.0.0.1:9876/api/ui/cards > /tmp/cards.json

# 5. Verify data consistency — card count matches SQLite
curl -s "http://127.0.0.1:9876/api/data/query?sql=SELECT+COUNT(*)+as+cnt+FROM+cards"

# 6. Visual verification — see "Visual Verification with /screenshot-capture" section above
```

### Testing Discovery Navigation

```bash
# Switch tabs and verify
curl -s -X POST -d '{"name":"tab-clicked","args":[1]}' http://127.0.0.1:9876/api/ui/callback
curl -s http://127.0.0.1:9876/api/ui/property/active-mode-index
# Capture screenshot: powershell -ExecutionPolicy Bypass -File "$SKILL_PATH/scripts/capture.ps1" -Window "WITwhat" -Output tab-switch.png

# Test search
curl -s -X POST -d '{"name":"search-changed","args":["test"]}' http://127.0.0.1:9876/api/ui/callback
curl -s http://127.0.0.1:9876/api/ui/property/search-text
curl -s http://127.0.0.1:9876/api/ui/property/card-count
# Capture screenshot: powershell -ExecutionPolicy Bypass -File "$SKILL_PATH/scripts/capture.ps1" -Window "WITwhat" -Output search-results.png

# Reset
curl -s -X POST -d '{"name":"esc-pressed","args":[]}' http://127.0.0.1:9876/api/ui/callback
curl -s -X POST -d '{"name":"esc-pressed","args":[]}' http://127.0.0.1:9876/api/ui/callback
```

### Debugging a Specific Card

```bash
# Find card by recipient name
curl -s http://127.0.0.1:9876/api/ui/cards | python -c "
import sys, json
cards = json.load(sys.stdin)
for i, c in enumerate(cards):
    if 'alice' in c.get('recipient_name','').lower():
        print(f'Index {i}: {json.dumps(c, indent=2)}')
"

# Check the recipient in SQLite
curl -s "http://127.0.0.1:9876/api/data/query?sql=SELECT+*+FROM+recipients+WHERE+recipient_id='RECIPIENT_ID_HERE'"
```

### Testing Product Catalog

```bash
# Navigate to Products tab (index 3)
curl -s -X POST -d '{"name":"tab-clicked","args":[3]}' http://127.0.0.1:9876/api/ui/callback

# Inspect product tiles in the grid
curl -s http://127.0.0.1:9876/api/ui/product-tiles

# Click a product tile to enter filtered card view + show sidecar
curl -s -X POST -d '{"name":"tile-clicked","args":["ProductName"]}' http://127.0.0.1:9876/api/ui/callback

# Inspect sidecar state and units
curl -s http://127.0.0.1:9876/api/ui/detail-units

# Open the add product form
curl -s -X POST -d '{"name":"on-product-add-clicked","args":[]}' http://127.0.0.1:9876/api/ui/callback

# Verify form is visible
curl -s http://127.0.0.1:9876/api/ui/property/add-product-visible

# Set product name and submit
curl -s -X PUT -d '{"value":"Test Product"}' http://127.0.0.1:9876/api/ui/property/add-product-name
curl -s -X POST -d '{"name":"on-add-product-submit","args":[]}' http://127.0.0.1:9876/api/ui/callback

# Check for validation errors
curl -s http://127.0.0.1:9876/api/ui/property/add-product-validation-error

# Discard and close
curl -s -X POST -d '{"name":"on-add-product-discard","args":[]}' http://127.0.0.1:9876/api/ui/callback

# Dismiss the lookup modal programmatically
curl -s -X PUT -d '{"value":false}' http://127.0.0.1:9876/api/ui/property/lookup-modal-open

# Test sidecar callbacks
curl -s -X POST -d '{"name":"on-product-sidecar-view-shopify","args":["product-id"]}' http://127.0.0.1:9876/api/ui/callback
curl -s -X POST -d '{"name":"on-product-sidecar-view-issue","args":["product-id"]}' http://127.0.0.1:9876/api/ui/callback

# Create a serial unit with custom SN
curl -s -X POST -d '{"name":"on-product-create-unit-with-serial","args":["product-id","SN-001"]}' http://127.0.0.1:9876/api/ui/callback

# Toggle unit selection
curl -s -X POST -d '{"name":"on-product-unit-selection-changed","args":["product-id","SN-001"]}' http://127.0.0.1:9876/api/ui/callback
```

### Automated GUI Interaction

Use element geometry + click simulation to drive the UI programmatically when callbacks alone are insufficient (e.g. clicking a specific pixel region, dismissing a tooltip, or interacting with elements that have no registered callback).

```bash
# 1. Get window dimensions to plan click targets
curl -s http://127.0.0.1:9876/api/ui/element/window
# {"name":"window","x":0,"y":0,"width":1200,"height":800}

# 2. Click a known UI coordinate (e.g. top-right close button area)
curl -s -X POST -d '{"x":1180,"y":20}' http://127.0.0.1:9876/api/ui/click

# 3. Verify the resulting state via data endpoints
curl -s http://127.0.0.1:9876/api/ui/property/settings-modal-open

# 4. Capture screenshot to confirm visual result
powershell -ExecutionPolicy Bypass -File "$SKILL_PATH/scripts/capture.ps1" -Window "WITwhat" -Output after-click.png
```

**Tips:**
- Use `GET /api/ui/element/window` to confirm actual window dimensions before computing click targets — window size may vary across machines.
- Prefer registered callbacks (`POST /api/ui/callback`) over raw clicks when available; clicks are for cases with no callback counterpart.
- After a click, add a short poll loop on the expected state change rather than a fixed sleep.

## Visual Verification with `/screenshot-capture`

BUGSWEEPER gives you programmatic access to UI state, but visual verification is essential for catching layout bugs, rendering glitches, and confirming what the user actually sees. Use the `/screenshot-capture` skill alongside BUGSWEEPER endpoints.

**How it works:** The skill uses PowerShell scripts (`.NET System.Drawing`) to capture windows. Claude reads the resulting PNG natively (multimodal vision — no OCR library needed). Zero external dependencies.

### Capturing the App Window

```bash
# 1. Find the window title (don't guess)
powershell -ExecutionPolicy Bypass -File "$SKILL_PATH/scripts/capture.ps1" -ListWindows

# 2. Capture by title substring
powershell -ExecutionPolicy Bypass -File "$SKILL_PATH/scripts/capture.ps1" -Window "WITwhat" -Output witwhat.png

# 3. Read the PNG with Claude's Read tool — Claude sees the image directly
# Read tool → the output path from step 2
```

`$SKILL_PATH` is `C:\Users\decid\.claude\skills\screenshot-capture`. The `-Output` path is relative to `Documents\Claude Screenshots\` unless absolute.

### Typical BUGSWEEPER + Screenshot Pattern

```bash
# Programmatically change state via BUGSWEEPER
curl -s -X POST -d '{"name":"tab-clicked","args":[2]}' http://127.0.0.1:9876/api/ui/callback

# Verify data layer reflects the change
curl -s http://127.0.0.1:9876/api/ui/property/active-mode-index

# Capture screenshot to verify the visual result
powershell -ExecutionPolicy Bypass -File "$SKILL_PATH/scripts/capture.ps1" -Window "WITwhat" -Output after-tab-switch.png
# Then: Read tool on the output path
```

### Annotating Screenshots

Write an annotations JSON file, then run the annotate script to highlight issues:

```bash
# Write annotations (rect, arrow, text, circle, highlight supported)
# Then apply:
powershell -ExecutionPolicy Bypass -File "$SKILL_PATH/scripts/annotate.ps1" -Input witwhat.png -Output annotated.png -Annotations annotations.json
```

### Cleanup Rule

Create a dedicated folder for each debug session (e.g. `Documents/Claude Screenshots/session-2026-03-26/`) and save all captures there. When the session is done, delete the entire folder in one command. This avoids the token cost of deleting after every individual screenshot.

### Tips

- Always run `-ListWindows` first — don't guess window titles.
- If a capture comes back black, add `-Delay 1` to let the window render after activation.
- The capture uses `PrintWindow` and works even if the window is partially occluded.
- Combine with BUGSWEEPER data dumps: screenshot shows _what the user sees_, BUGSWEEPER shows _what the data says_. Discrepancies between the two are bugs.

## Error Handling

All endpoints return JSON. Errors use appropriate HTTP status codes:

| Code | Meaning |
|------|---------|
| 200 | Success |
| 400 | Bad request (invalid JSON body, missing parameters) |
| 404 | Unknown endpoint (response includes list of available endpoints) |
| 500 | Backend error (UI thread timeout, invalid property name, SQL error) |

The UI thread bridge has a 5-second timeout. If the Slint event loop is blocked (e.g. modal dialog), UI endpoints will return a 500 with "UI query timed out".

## Architecture Notes

- Server runs on a background thread; all UI access goes through `slint::invoke_from_event_loop` + mpsc channel
- SQLite queries share the app's existing `Arc<SqliteStore>` — no separate connection
- `BugsweeperBackend` trait is defined in `crates/bugsweeper/src/router.rs`, implemented in `crates/app/src/main.rs`
- The entire module is behind `#[cfg(feature = "bugsweeper")]` — zero production overhead
- Properties and callbacks are registered in `PROPERTY_REGISTRY` and `CALLBACK_REGISTRY` const tables in `crates/app/src/main.rs`. Adding new Slint properties/callbacks to BUGSWEEPER requires: (1) add entry to the registry table, (2) add match arm in the corresponding get/set/invoke function.

## Self-Healing Protocol

If a BUGSWEEPER debug session cannot complete all required test tasks due to missing endpoints, unregistered properties/callbacks, blocked queries, or other framework limitations:

1. **Stop and fix BUGSWEEPER first.** Run `/gsd:quick` to address the shortcoming(s). Prefer broadly applicable solutions over narrow point-fixes — your solution should handle similar future scenarios, not just the exact gap encountered now.
2. **Resume testing.** After the quick fix is committed, rebuild with `--features bugsweeper` and resume the debug session using the newly improved BUGSWEEPER to complete the remaining tasks.
3. **Preserve modularity.** All improvements must stay behind `#[cfg(feature = "bugsweeper")]`, must not add dependencies to the core app, and must not bloat production builds.
