Debug bridge
SkillWeb & browsingDrive 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.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
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
nonMinifiedReleaseinstall answers nothing. - Install with
./gradlew :app:installDebug. Don'tadb installtheintermediates/apkfile, 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.shdelivers to it anyway (-f 0x20). A hand-writtenam broadcastwithout 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
STATEon each device: noteself.nodeIdand confirm the other side is inreachable[]. A peer that isn't there is a radio problem, not a delivery one. Readtransports[](per-radiohealth,linked,nearby) first, thenmeshEnabled,meshPausedandmeshStartDeferred, any of which leaves a mesh that looks alive with no peers.- Act on A, e.g.
SEND --es text '<unique body>' --es conv <nearby | peerNodeId | g-…>. Use a body you can grep for (prefix itsoakorburstsoPURGEcan clean it up later). - Verify on B with
STATE --es conv <id>: the body appears inmessages[]. Then, if the test is about the ✓✓, read A's own row (mine: true) until itsreceivedflips: 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 baresleep: 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
| Question | Action | Read |
|---|---|---|
| Did it arrive? | STATE --es conv <id> on the receiver | the body in messages[] |
| Did the ✓✓ get home? | STATE --es conv <id> on the sender | its own row's (mine: true) received |
| Did a reaction or typing cue land? | STATE on the receiver | messages[].reactions; typing |
| Do two nodes carry the same custody? | STORE on each | liveFingerprint equal across devices (never allFingerprint); digestVersion == liveFingerprint on each |
| Which frame is stranded? | STORE | allIds sorted per device, then comm between devices |
| Is the Internet plane converged? | SPOOL | per scope local == spool, invalid 0, lastError empty, connected |
| Is a DM session healthy? | RATCHET | hasSession and confirmed; peerPrekeyPinned before any reset |
| LoRa range or airtime? | LORA | loraSent/loraReceived, loraNakByReason |
| Why didn't a transfer start? | XFER | refusal first, then the live transfers |
| Is the mesh paused? | STATE | meshPaused / 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 withrun-asfirst),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=0with nodata=, and nothing underKnitBridgeusually 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 0x20it can be a stopped package.- A text cut off at its first space means it went through a raw
adb shellwithout the inner quotes. missing 'path' extra, or a path the app can't read: scoped storage means the app can't read/sdcardor/data/local/tmp. Copy the file in withadb 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.errorfromCOMMONSmeans 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
- A branch in the
wheninDebugBridgeReceiverand anACTION_constant. - An
<action>in the receiver's<intent-filter>inapp/src/debug/AndroidManifest.xml. Without it the broadcast is never delivered, and nothing errors. - A bullet in
.agents/context/debug-bridge.mdcovering the extras, the reply fields, what the oracle is, and the negative control if it injects a fault. - 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
Related picks
Skill · a5c-ai
The pick for Java110-java-maven-best-practices
Skill · jabrena
The pick for Javakotlin-tooling-agp9-migration
Skill · kotlin
The pick for Kotlinapollo-kotlin
Skill · apollographql
The pick for Kotlinreact-component-performance
Skill · davila7
The pick for Reactreact-doctor
Skill · millionco
The pick for React