| name | closed-loop-debug |
| description | Complete closed-loop debugging workflow for LLM agents working on nostrc GTK applications. Covers the full cycle: identify bug → write/run test → debug with GDB/LLDB → fix code → rebuild → verify via Broadway UI → iterate. Integrates the Broadway and GDB skills into a unified methodology that an LLM can execute deterministically.
|
| allowed-tools | Bash,Read,mcp__playwright__*,mcp__RepoPrompt__* |
| version | 1.0.0 |
Closed-Loop Debug Workflow for nostrc GTK Apps
A deterministic methodology for LLM agents to identify, reproduce, diagnose, fix,
and verify bugs in gnostr, gnostr-signer, and other GTK4 applications in the nostrc
stack — without human intervention between iterations.
The Loop
┌──────────────────────────────────────────────────────────┐
│ │
▼ │
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ IDENTIFY │───►│ REPRO │───►│ DIAGNOSE│───►│ FIX │───►│ VERIFY │
│ (test/ │ │ (test + │ │ (GDB/ │ │ (edit │ │ (test + │
│ report) │ │ ASan) │ │ LLDB/ │ │ code) │ │ Bway) │──┐
└─────────┘ └─────────┘ │ ASan) │ └─────────┘ └─────────┘ │
└─────────┘ │ │
│ PASS │ FAIL
▼ │
DONE ◄─────┘
Each phase uses specific tools and produces structured output that feeds the next phase.
Phase 1: IDENTIFY — Find the Bug
From a failing test
cd build && ctest --output-on-failure 2>&1 | tail -50
ctest -R nostr_gtk --output-on-failure
ctest -R ndb-main-thread --output-on-failure
From an ASan report
cmake -B build-asan -DCMAKE_BUILD_TYPE=Debug \
-DGNOSTR_ENABLE_ASAN=ON -DGNOSTR_ENABLE_UBSAN=ON
cmake --build build-asan
ASAN_OPTIONS=detect_leaks=1 \
build-asan/apps/gnostr/gnostr 2>&1 | tee /tmp/asan-report.txt
From Broadway UI observation
./skills/broadway-debug/scripts/run-broadway.sh
From a beads issue
bd ready
bd show <issue-id>
bd update <id> --status in_progress
Output of this phase: A clear statement of what's wrong, ideally with a
reproduction path (test command, ASan trace, or UI steps).
Phase 2: REPRODUCE — Make It Deterministic
The goal is to have a single command that demonstrates the bug every time.
For crashes / memory errors
build-asan/apps/gnostr/tests/gnostr-test-ndb-main-thread-violations
For UI bugs
GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build/apps/gnostr \
build/apps/gnostr/gnostr &
For latency / performance bugs
build/apps/gnostr/tests/gnostr-test-ndb-main-thread-violations 2>&1
xvfb-run -a build/nostr-gtk/tests/test_nostr_gtk_bind_latency_budget 2>&1
Output: A single command that fails consistently.
Phase 3: DIAGNOSE — Find the Root Cause
Strategy selection
| Bug Type | Primary Tool | Secondary Tool | Inspector Panel |
|---|
| Segfault | ASan build | GDB/LLDB bt full | — |
| Use-after-free | ASan (shows alloc+free) | GDB watchpoint | Objects (signal handlers) |
| Memory leak | LSAN / Valgrind | GObject ref count tracing | Objects (instance count) |
| Main-thread blocking | NDB violation test | GDB breakpoint on storage_ndb_begin_query | Statistics (frame times) |
| Widget sizing | GTK Inspector Visual panel | gtk_widget_measure() in test | Visual + CSS |
| Signal handler bug | GTK Inspector Objects panel | GDB break on g_signal_* | Objects (Signals section) |
| Latency | Heartbeat test | GDB + NDB violation count | Statistics + Recorder |
GDB diagnosis (Linux)
gdb -batch \
-ex "set pagination off" \
-ex "set print pretty on" \
-ex "run" \
-ex "bt full" \
-ex "info threads" \
-ex "thread apply all bt 10" \
--args build-debug/apps/gnostr/tests/FAILING_TEST 2>&1
LLDB diagnosis (macOS)
lldb -b \
-o "run" \
-o "bt all" \
-o "thread list" \
-k "bt all" \
-k "quit" \
-- build-debug/apps/gnostr/tests/FAILING_TEST 2>&1
ASan diagnosis (cross-platform)
ASAN_OPTIONS=detect_leaks=1:abort_on_error=0 \
build-asan/apps/gnostr/tests/FAILING_TEST 2>&1
Output: The specific function, line number, and root cause mechanism.
Phase 4: FIX — Apply the Change
Common fix patterns
Use-after-free in callback:
g_signal_connect(source, "notify::profile", G_CALLBACK(on_profile), row);
g_object_weak_ref(G_OBJECT(row), (GWeakNotify)invalidate_row_ref, ctx);
self->profile_handler_id = g_signal_connect(...);
if (self->profile_handler_id) {
g_signal_handler_disconnect(item, self->profile_handler_id);
self->profile_handler_id = 0;
}
Main-thread NDB transaction:
const char *content = storage_ndb_get_content(key);
static void query_in_thread(GTask *task, ...) {
const char *content = storage_ndb_get_content(key);
g_task_return_pointer(task, g_strdup(content), g_free);
}
static void on_query_done(GObject *src, GAsyncResult *res, gpointer data) {
char *content = g_task_propagate_pointer(G_TASK(res), NULL);
}
g_task_run_in_thread(task, query_in_thread);
Memory leak (missing unref):
GObject *obj = g_object_new(MY_TYPE, NULL);
some_function(obj);
g_autoptr(GObject) obj = g_object_new(MY_TYPE, NULL);
some_function(obj);
Apply the edit
# Use apply_edits with verbose=true to see the diff
apply_edits(path="src/model/gn-nostr-event-model.c",
search="old code...", replace="new code...", verbose=true)
Phase 5: VERIFY — Confirm the Fix
Step 1: Rebuild
cmake --build build-debug
cmake --build build-asan
Step 2: Run the reproducing test
build-debug/apps/gnostr/tests/FAILING_TEST
build-asan/apps/gnostr/tests/FAILING_TEST
Step 3: Run the full test suite (no regressions)
cd build-debug && ctest --output-on-failure
Step 4: Visual verification via Broadway (if UI-related)
GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build-debug/apps/gnostr \
build-debug/apps/gnostr/gnostr &
Step 5: Decision
- PASS: All tests green, UI looks correct → proceed to commit
- FAIL: Loop back to Phase 3 (diagnose) with new information
Complete Example: Fixing a Recycling Crash
=== IDENTIFY ===
Running: ctest -R listview_recycle --output-on-failure
Result: FAIL — "profile notification after unbind" test crashes
=== REPRODUCE ===
Command: build-asan/nostr-gtk/tests/test_nostr_gtk_listview_recycle_stress
ASan output:
heap-use-after-free in on_profile_changed (note-card-factory.c:312)
freed by factory_unbind_cb (note-card-factory.c:280)
=== DIAGNOSE ===
Reading note-card-factory.c:312 — the `notify::profile` handler
fires AFTER unbind. The handler_id was disconnected, but there's a
SECOND handler connected in on_ncf_row_mapped_tier2() that uses a
different signal name and wasn't tracked.
Root cause: `on_ncf_row_mapped_tier2` connects `notify::profile`
on the ITEM with ROW as user_data, but `factory_unbind_cb` only
disconnects handlers tracked in `row->profile_handler_id` — the
tier-2 handler has a different ID stored in a local variable that's
lost when the stack frame exits.
=== FIX ===
Store the tier-2 handler ID in the row struct.
Disconnect it in factory_unbind_cb.
apply_edits(path="apps/gnostr/src/ui/note-card-factory.c", ...)
=== VERIFY ===
cmake --build build-asan
build-asan/nostr-gtk/tests/test_nostr_gtk_listview_recycle_stress → PASS ✅
cd build-asan && ctest --output-on-failure → ALL PASS ✅
App-Specific Commands
gnostr
build-debug/apps/gnostr/gnostr
GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build-debug/apps/gnostr \
build-debug/apps/gnostr/gnostr
cd build-debug && ctest -R gnostr --output-on-failure
gnostr-signer
build-debug/apps/gnostr-signer/gnostr-signer
GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build-debug/apps/gnostr-signer \
build-debug/apps/gnostr-signer/gnostr-signer
cd build-debug && ctest -R signer --output-on-failure
Any GTK app in the stack
The same patterns apply to any GTK4 app built with the nostrc build system:
GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 \
GSETTINGS_SCHEMA_DIR=build-debug/apps/<APP_NAME> \
build-debug/apps/<APP_NAME>/<BINARY>
Iteration Tracking
For multi-iteration debug sessions, use beads to track progress:
bd update <id> --status in_progress
bd update <id> --note "Iteration 1: ASan shows UAF in factory_unbind_cb"
bd update <id> --note "Iteration 2: Found missing handler disconnect in tier-2 path"
bd update <id> --note "Iteration 3: Fix applied, all tests pass"
bd close <id> --reason "Fixed handler disconnect in note-card-factory.c"
bd sync
Parallel Debugging
For complex bugs that span multiple components:
Terminal layout
┌─────────────────────┬─────────────────────┐
│ Terminal 1: │ Terminal 2: │
│ Broadway app │ GDB session │
│ (visual feedback) │ (crash diagnosis) │
├─────────────────────┼─────────────────────┤
│ Terminal 3: │ Terminal 4: │
│ Test runner │ Code editor │
│ (regression check) │ (apply fixes) │
└─────────────────────┴─────────────────────┘
LLM agent workflow (single terminal)
./skills/broadway-debug/scripts/run-broadway.sh &
while ! build-debug/tests/FAILING_TEST; do
cmake --build build-debug --target FAILING_TEST
done
GTK Test Utilities Reference
For writing deterministic widget tests (used in Phase 2):
| Function | Purpose |
|---|
gtk_test_init(&argc, &argv, NULL) | Initialize GTK in test mode |
gtk_test_widget_wait_for_draw(w) | Wait for pending redraws |
gtk_widget_measure(w, orient, for_size, ...) | Measure widget dimensions |
gtk_test_accessible_assert_role(w, role) | Verify accessibility role |
gtk_test_accessible_assert_property(w, ...) | Verify accessible properties |
g_test_add_func(path, func) | Register a test case |
g_test_run() | Run all registered tests |
Related