debugging — working out what a deployed app is actually doing
prompts/pkg/llms/debugging.md on every deploy, so it
can't drift from what the platform actually does. Read it from your terminal any
time with npx vibes-diy skills.
This guide is for you, the agent, when something in a live app does not match what the code says it should do. It is the method, not a feature the app author types.
The loop
- Reproduce in words. Say what the person did, what they expected and what they saw. A reproduction you can state is a reproduction you can check.
- Read the release state. Which version is serving, whether newer work is waiting behind it, whether the version that is live carries the record rules and the server file you think it does. A change that was never released is the most common explanation for "I fixed that already".
- Read the logs. What the deployed app has been recording about itself, newest first. A line the app wrote beats an expectation about the code.
- Read the records. The data settles which of two stories is true. Read it as the person whose app it is, so you see what their app shows them.
- One hypothesis. Name the single thing you now believe is wrong, in one sentence.
- The smallest change that tests it. One record, one line, one handler.
- Verify from a read. Read the thing back after the change. A claim rests on what a read returned, never on the fact that a call succeeded.
Instrumentation the platform already has
ctx.log writes a line from server code, and vibe.log writes one from the
page. Both land in the app's own log stream, stamped with the version that
produced them, so a deploy boundary is visible in the stream.
jsexport async function scheduled(ctx) {
ctx.log("info", "tick start", { at: new Date().toISOString() });
const page = await ctx.db.query("kind", { key: "rsvp", limit: 100, db: "rsvps" });
ctx.log("info", "tick read", { count: page.length });
}
A probe that only paints its result on screen is unreadable from outside the app, so a self-checking probe echoes each case on one line:
jsconsole.log(`[RSVP] ${ok ? "PASS" : "FAIL"} duplicate-guard ${JSON.stringify({ id, count })}`);
Start the run with a marker — console.log("===== START =====") — so a later
read knows which lines belong to this attempt.
Bulk work is a route, not a loop
Changing a handful of records one at a time is fine. Past that, write the change once, as a named route in the app's own server file, and have the person run it. A route runs beside the data, keeps its own progress, and can be read and re-read before it is trusted.
Gate the route inside the handler on the caller being the owner, declare
config.fetch.unfilteredReads for the collections it reads, page with a cursor,
and remember progress in a state document read by id so a second run continues
rather than starting over. An empty page can still carry next, so loop on the
cursor.
jsexport const config = { fetch: { unfilteredReads: ["rsvps"] } };
export async function fetch(request, ctx) {
const url = new URL(request.url);
if (url.pathname !== "/admin/normalize") return new Response("not found", { status: 404 });
if (ctx.user?.userHandle !== ctx.ownerHandle) return new Response("forbidden", { status: 403 });
const state = (await ctx.db.get("0-normalize-state", { db: "rsvps" })) ?? { _id: "0-normalize-state", after: null };
let after = state.after;
let changed = 0;
for (let round = 0; round < 20; round++) {
const page = await ctx.db.query("kind", { key: "rsvp", limit: 100, ...(after ? { after } : {}), db: "rsvps" });
for (const doc of page) {
if (typeof doc.name === "string" && doc.name !== doc.name.trim()) {
await ctx.db.put({ ...doc, name: doc.name.trim() }, { db: "rsvps" });
changed++;
}
}
if (!page.next) { after = null; break; }
after = page.next;
}
await ctx.db.put({ ...state, after, at: Date.now() }, { db: "rsvps" });
ctx.log("info", "normalize round", { changed, after });
return new Response(JSON.stringify({ ok: true, changed, more: after !== null }), {
headers: { "content-type": "application/json" },
});
}
Once the route is published, run it with call_own_api: it calls the app's own
routes as the person whose app this is, so the handler sees the owner and the
answer comes back in the conversation. Read the release state first when unsure
which routes the live version carries, and read the records back afterwards to
confirm what the run did.
The person can run the same route from their own machine with their own credential:
shcurl -sS -X POST \
-H "Authorization: Bearer $VIBES_TOKEN" \
https://vibes.diy/vibe/<owner>/<slug>/_api/admin/normalize
A route is live code
backend.js and access.js always run the app's live release, so a route has
to be published before anyone can call it, and publishing it puts it in front of
everyone the app serves. Try a route whose effect you are unsure of under a
second address first, confirm what it did by reading the records back, then ship
it where the real data is. call_own_api reaches the live release too, so what
it runs is what everyone else is being served.