Debug bridge

SkillWeb & browsing

Drive and verify Knit on a device or emulator through the headless debug bridge (`am broadcast` to `app.getknit.knit.debug.<ACTION>`, replies as JSON), send a message on one phone and confirm it landed on another without screenshots, read mesh state, check custody / spool / ratchet convergence, run a LoRa, direct-transfer or Wi-Fi Aware fault trial on a locked lab device, or post a fixture notification for a UI test. Use whenever a task says run it on a device, check it on the phones, lab trial, device trial, soak, convergence check, `KnitBridge`, or names any `…debug.<ACTION>`; and before adding a bridge action.

Use Debug bridge in Claude, ChatGPT or Ahel Desktop

Free. Sign in, add Debug bridge and connect your AI. About a minute.

Also: Claude Code · Cursor · Codex

Then ask your AI: use the Debug bridge skill

Details

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Debug bridgeStart free

What this skill tells your AI

The instructions your AI receives, as published by getknit/knit in .agents/skills/debug-bridge/SKILL.md and read by ahel’s review.

Debug builds carry an exported receiver (app/src/debug/.../debug/DebugBridgeReceiver.kt) that calls the mesh directly and answers in JSON, so an agent can drive the send→verify loop with no UI and no screenshots. This skill is the procedure. The per-action extras and reply fields are in .agents/context/debug-bridge.md, and this file points into it rather than copying it.

Gate 0: whether you may

Obey .agents/rules/devices.md. An emulator is fair game. A physical phone, including every lab phone on network adb, needs the user's go-ahead for this task, every time. Being reachable in adb devices is not permission, and neither is a yes from an earlier task. Never toggle Wi-Fi over adb on a lab phone. Always pin the target with -s <serial> or ANDROID_SERIAL: several devices are usually attached.

Gate 1: a debug build that matches

  • The bridge exists only in debug builds. A release or nonMinifiedRelease install answers nothing.
  • Install with ./gradlew :app:installDebug. Don't adb install the intermediates/apk file, which AGP 9 never rewrites.
  • A lab phone may run a feature branch with a newer Room schema than yours, and installing over it half-runs the app. Check what the device has before you install, and say what you replaced.
  • After an install the package is in the stopped state. scripts/bridge.sh delivers to it anyway (-f 0x20). A hand-written am broadcast without that flag gets an empty reply until the app is opened.

The call

scripts/bridge.sh <serial|-> <ACTION> [am-extras…]      # JSON on stdout; "-" = $ANDROID_SERIAL
scripts/bridge.sh emulator-5554 STATE | jq '{self, health, reachable}'
scripts/bridge.sh - SEND --es text 'hi there 😀' --es conv nearby

The script single-quotes every extra for the device shell, adds -f 0x20 and -p app.getknit.knit, pulls the data="…" out of Broadcast completed, and falls back to the receiver's KnitBridge:I log mirror (only lines newer than the call). It exits 1 with a hint on stderr when nothing parsable came back. A refusal is still a reply, though: a missing extra answers {"status":"error","message":"missing 'path' extra"} with exit 0, so check .status (jq -e '.status == "ok"') before reading fields. Extras use am's own flags: --es string, --ei int, --ez bool.

If you must write the raw form, quote the whole remote command and single-quote the values. adb re-parses on the device, so a bare --es text "hi there" arrives as hi:

adb -s A shell "am broadcast -f 0x20 -a app.getknit.knit.debug.SEND -p app.getknit.knit --es text 'hi there' --es conv nearby"

The core loop

  1. STATE on each device: note self.nodeId and confirm the other side is in reachable[]. A peer that isn't there is a radio problem, not a delivery one. Read transports[] (per-radio health, linked, nearby) first, then meshEnabled, meshPaused and meshStartDeferred, any of which leaves a mesh that looks alive with no peers.
  2. Act on A, e.g. SEND --es text '<unique body>' --es conv <nearby | peerNodeId | g-…>. Use a body you can grep for (prefix it soak or burst so PURGE can clean it up later).
  3. Verify on B with STATE --es conv <id>: the body appears in messages[]. Then, if the test is about the ✓✓, read A's own row (mine: true) until its received flips: that flag is the delivery tick coming home to the sender, not something the receiver's row carries. Poll in a bounded loop. Never trust one bare sleep: delivery order and timing across radios are not promised.
for i in $(seq 1 30); do
  scripts/bridge.sh B STATE --es conv nearby | jq -e --arg b "$BODY" '.messages[] | select(.body == $b)' && break
  sleep 2
done

Which oracle answers which question

QuestionActionRead
Did it arrive?STATE --es conv <id> on the receiverthe body in messages[]
Did the ✓✓ get home?STATE --es conv <id> on the senderits own row's (mine: true) received
Did a reaction or typing cue land?STATE on the receivermessages[].reactions; typing
Do two nodes carry the same custody?STORE on eachliveFingerprint equal across devices (never allFingerprint); digestVersion == liveFingerprint on each
Which frame is stranded?STOREallIds sorted per device, then comm between devices
Is the Internet plane converged?SPOOLper scope local == spool, invalid 0, lastError empty, connected
Is a DM session healthy?RATCHEThasSession and confirmed; peerPrekeyPinned before any reset
LoRa range or airtime?LORAloraSent/loraReceived, loraNakByReason
Why didn't a transfer start?XFERrefusal first, then the live transfers
Is the mesh paused?STATEmeshPaused / meshPausedUntil

Each row's caveats (the retiring and accounted spool fields, the TTL edge on allFingerprint) are in the context doc. Read its bullet before you call a result converged or diverged.

Action index

Extras and reply fields: the action's bullet in .agents/context/debug-bridge.md under Headless bridge.

  • Messaging: SEND, SENDIMG (stage the file into app storage with run-as first), REACT, TYPING, MKGROUP, LEAVE, PURGE (clean soak traffic off every phone).
  • Mesh state: STATE, STORE, HEAL, PAUSE, RESUME, BLECAP, NANOFF (Wi-Fi Aware off, Bluetooth alone), PHY (the Coded PHY experiment).
  • Planes: SPOOL, COMMONS, LORA, LORATX, LORAPROV, XFER.
  • Identity, crypto, data: INTRO (contact cards), RATCHET, BACKUP.
  • Fault injection and trials: NANFAIL, NANSTORM, NANREFUSE, NANICM, NANDIAL, NANINIT, NANMSG, MODEL. Each has a negative control in its bullet; run it too.
  • UI-test fixtures: FLAGMSG, MSGNOTIF, REQNOTIF, REVIEW.
  • Media and packaging probes: WEBPPROBE, WEBPCONV, WEBPCHECK, SHAREAPK.

When it answers nothing

  • result=0 with no data=, and nothing under KnitBridge usually means the action isn't in the debug manifest's <intent-filter>, or is misspelt. It can also mean a non-debug build. Without -f 0x20 it can be a stopped package.
  • A text cut off at its first space means it went through a raw adb shell without the inner quotes.
  • missing 'path' extra, or a path the app can't read: scoped storage means the app can't read /sdcard or /data/local/tmp. Copy the file in with adb shell "cat /data/local/tmp/x | run-as app.getknit.knit sh -c 'cat > files/x'" and pass /data/data/app.getknit.knit/files/x.
  • error from COMMONS means the build hides the commons (BuildConfig.COMMONS).
  • A reply that looks healthy while nothing moves: read the oracle's caveat fields (lastError, refusal, declined). Most gates in the mesh return silently, and these fields exist to tell them apart.

When the UI is unavoidable

Use the stable resource-ids, cold-start navigation (--es demo_route chat/<id>), and the rule that popups, dialogs and bottom sheets don't surface their test tags. These are the Stable resource-ids and Cold-start navigation sections of the context doc. Prefer a bridge action over driving a sheet. XFER exists partly so nobody has to drive the consent sheet.

Adding an action

  1. A branch in the when in DebugBridgeReceiver and an ACTION_ constant.
  2. An <action> in the receiver's <intent-filter> in app/src/debug/AndroidManifest.xml. Without it the broadcast is never delivered, and nothing errors.
  3. A bullet in .agents/context/debug-bridge.md covering the extras, the reply fields, what the oracle is, and the negative control if it injects a fault.
  4. A word in this skill's action index.

Check for drift after any change. This must print nothing:

comm -3 <(grep -oE '"app\.getknit\.knit\.debug\.[A-Z]+"' app/src/debug/java/app/getknit/knit/debug/DebugBridgeReceiver.kt | tr -d '"' | sed 's/.*\.//' | sort -u) \
        <(grep -oE 'debug\.[A-Z]+' .agents/context/debug-bridge.md | sed 's/.*\.//' | sort -u)

Signals

GitHub stars
122
Forks
9
Last commit
Oct 2026
Advanced
Item type
skill
Key
debug-bridge
Source
github.com/getknit/knit