For weeks this summer, a bot posted a twice-daily bug report into a Slack channel nobody read — the same five bugs every time, the oldest 169 days old, in a repository I'd already retired. Then it stopped on its own, and nobody noticed that either. That zombie is what a dead agent loop looks like, and it's why round two of this system was built audit-first.
The Skate Workshop is a React Native coaching app I'm rebuilding for skateboarding coach Willy Santos — in TestFlight beta on iOS (not yet submitted to the App Store or Play Store), with a 4,952-combination trick grid and 175 tests in CI. The development method is the one I run across my projects: Claude Code writes on its own branches, every PR lands behind a human merge gate, and a green build auto-publishes an over-the-air update. This article is about the other half — the feedback loop that turns an athlete's bug report into an issue, a fix, and a shipped update. Version one of that half died in public. Round two got relit this week, and I have the receipts.
The Loop, As Designed#
One loop, two halves. The GitHub half turns merged code into shipped updates. The Slack half turns athlete feedback into GitHub issues. The full circuit:
Report.
An athlete taps Report a bug in the app — a screenshot goes to a private bucket, a row lands in the bug_reports table. Or they just type the bug into the intake channel and a listener picks it up.
File.
A database webhook fires an edge function that opens a GitHub issue — labeled by category, prioritized by a keyword heuristic — and posts the link back into Slack.
Triage.
An agent labels, dedupes, and posts triage notes back to the channel.
Fix.
Claude writes a failing test first, fixes on its own branch, and opens a PR labeled needs-joe.
Gate.
Only I merge — optionally by typing /deploy go in Slack to batch-dispatch approved PRs.
Ship.
Any merge to main runs 175 tests. A green build auto-publishes an over-the-air update to the beta's preview channel, and a ship notice — update-group ID plus rollback command — goes to Slack.
That's the design. On September 1 I audited what was actually true.
What a Dead Loop Looks Like#
Version one of this loop ran against the original repo starting in February, and the intake genuinely worked: athletes filed real reports, issue numbers climbed past #150, and 37 in-app bug reports flowed through the pipe. I still have every one of them, preserved in a backup schema in the current database. Then I retired that repo and rebuilt the app from scratch as v3 — and every deployed piece of the Slack half kept pointing at the corpse.
Jan 2026
Workspace built
A Slack workspace purpose-built for the beta: an intake channel with a pinned bug-report template, a feature-request channel, a dev channel, and a bot user.
Feb 2026
v1 loop live
Three edge functions deployed: in-app reports become GitHub issues, Slack messages become GitHub issues, /deploy dispatches approved PRs. Athletes file real bugs; 37 reports flow through the pipe.
Spring
The rebuild
I retire the v1 repo and rebuild the app as v3. Development moves. The deployed Slack loop doesn't — every function still points at the retired repo.
Aug 5
The zombie dies
The twice-daily Morning Bug Report posts one last time — the same five stale bugs, the oldest 169 days old, in a repo nobody will ever fix again — then stops. I still haven't identified what was running it.
Nobody noticed it running. Nobody noticed it stop.
Aug 19
The other lane dies too
CI starts failing on Expo SDK drift, which silently skips every auto-OTA publish. The ship lane is dead for 13 days and nothing says so.
Sep 1
The audit
A two-pass agent audit maps every component against the live systems. Same day: CI relit with warn-only drift gates, two playbook PRs merged, two OTA updates auto-published.
Sep 2
The relight
All three edge functions repointed at v3, hardened, and redeployed. A live test runs the pipe and stops at exactly one stale credential.

A bot faithfully reminding an empty room about five unfixable bugs in a dead repo is the whole cautionary tale in one image.
The Zeroes
The intake channel: silent for 27 days when the audit ran.
The dev and feature-request channels: zero messages. Ever.
The v3 bug_reports table: zero rows.
Mentions of the v3 repo anywhere in the workspace: zero.
PRs announced in Slack: zero of 13. OTA ships announced: zero of 2.
The Half That Worked#
While the Slack half rotted, the GitHub half quietly became the most productive lane I run. Claude has co-authored 44 of the 82 commits on main. Two of the thirteen merged PRs came off claude/* branches — both labeled needs-joe, both merged by me. The agent has never merged its own work. CI runs 175 tests on every push plus a Monday drift check, and a green run on main auto-publishes an over-the-air update to the beta's preview channel.
44/82
Commits Claude-co-authored
2/13
Merged PRs from claude/* branches
13
Days the OTA lane was silently dead
2
OTAs auto-published on Sep 1
It also produced this article's second cautionary tale. On August 19, CI started failing on Expo SDK drift — and a failed CI silently skips the OTA publish, so the ship lane was dead for 13 days with nothing to say so. The September 1 playbook audit caught it: warn-only drift gates turned CI green the same day, and both merges that followed auto-shipped OTA updates within hours. The loop's one working limb had itself been quietly dead for almost two weeks. And neither of those ships was announced in Slack — the loop's own comms rules require posting an update-group ID and a rollback command on every publish, and nothing is wired to do it yet.

The Audit, With Receipts#
Before rewiring anything, I ran a two-pass audit: one agent gathered the state of every component from the live systems — deployed edge-function source, database rows, the Slack API, GitHub's API — and a second agent adversarially re-derived every claim from scratch. Numbers that survived both passes went into the plan; the rest got hedged or cut. The verdict: the GitHub half genuinely works, and the Slack half was February scaffolding over a void.
| Component | State on September 1 |
|---|---|
| create-bug-issue (v17, deployed Feb 6) | Hardcoded to the retired v1 repo. No secret check — any caller could forge a payload. |
| slack-bug-listener (v8, deployed Feb 6) | Same dead repo. Publicly callable, no Slack signature verification. |
| slack-deploy-command (v10, deployed Feb 6) | Dispatches a batch-deploy workflow that doesn't exist in the v3 repo. |
| bug_reports webhook | Live and armed with a secret header — firing at a function that never checked it. |
| Corrected rewrite sitting on main | Written, never deployed — and carrying a wrong Slack channel ID of its own. |
| The intake channel | Silent for 27 days. Last post: the zombie. |
My favorite finding is the channel ID, because it's the most honest one: even the fixed version of the code — written specifically to revive this loop — routed feedback to a channel ID that matched nothing the audit could see. Nobody had ever run this pipe end to end. Code review doesn't catch that. Running it does.
The Relight#
On September 2, the Slack half got rewired into the live build — in this order, deliberately:
Security first.
These functions run with JWT verification off so webhooks and Slack's Events API can reach them — which makes their URLs publicly callable. A security review flagged that the Slack-facing functions would accept forged payloads. The redeployed versions fail closed: the database webhook must present a shared secret, and the Slack endpoints verify Slack's HMAC request signature — replay window, constant-time comparison, and refuse-everything if the secret is unset.
Repoint everything.
All three functions redeployed against the v3 repo: create-bug-issue v17 → v18, slack-bug-listener v8 → v9, slack-deploy-command v10 → v11.
Fix the channel ID.
The feedback route now points at the real dev channel, verified against the live Slack API — the correction and its date are documented in a code comment for the next auditor.
Fire a live test.
One real row through the real pipe, the way the app would send it. The webhook fired, the secret check passed, the function built the issue payload — and stopped at exactly one point: the stored GitHub credential is stale. That's the single remaining blocker, and rotating it is a step I only do by hand.
The pipe is proven. The last key is being cut.
Where It Stands#
3
Edge functions live against v3
v18 / v9 / v11
1
Test report through the pipe
0
Real athlete reports in v3 yet
1
Blocker left: one stale credential
I want to be precise about what's proven and what isn't. Proven: the GitHub lane end to end — Claude-authored PRs, gated merges, 175-test CI, two auto-OTA publishes on September 1 — and the intake pipe up to the GitHub call. Not yet proven: a real athlete report flowing through v3. The only row in the table is my test. Round two is relit, not finished.
Still Missing
The credential rotation — hand-done, in progress.
An agent actually bound to Slack: today, zero of my scheduled jobs deliver there.
A ship-notice step in the OTA workflow, so every publish posts its update-group ID and rollback command.
The batch-deploy workflow that /deploy go expects to dispatch.
A daily triage sweep for this project, like the one my other beta loop already runs.
Stack
Supabase Edge FunctionsDeno functions next to the database: webhook intake, Slack listener, /deploy command.
Database webhooksAn INSERT on bug_reports fires the intake function with a shared-secret header.
GitHub Actions175-test CI on every push plus a Monday drift check; green-only auto-OTA.
Expo / EAS UpdateOver-the-air JS updates to the TestFlight beta's preview channel.
SlackPurpose-built workspace: intake channel with a pinned report template, feature requests, dev channel.
Claude CodeWrites on claude/* branches — 44 of 82 commits co-authored. Never merges.
Lessons#
A loop is only alive if something notices it dying
Two limbs of this system died silently: the zombie poster ran for weeks after its repo was retired and then stopped without anyone noticing either event, and the CI-to-OTA lane was dead for 13 days because a skipped workflow makes no sound. More automation doesn't fix that — making silence itself an alarm does. The Monday drift check exists now; ship notices in Slack are next.
Intake is the easy half — it's been proven twice
37 real athlete reports flowed through v1's pipe. People will report bugs when the path is one tap in the app or one message in a channel. The hard half is everything after intake: keeping every deployed endpoint pointed at the living repo, and having an agent actually consume what arrives.
Hardcoded config outlives whatever it points at
The February functions baked in a repo name and channel IDs. The repo they named was retired, and the functions kept faithfully serving it for half a year. Anything that names an external target — repos, channels, workflows — deserves one config point and an audit that checks it against the live world, not a code review that checks it against intentions.
The relight is the moment for the security pass
Webhook-reachable endpoints can't require JWTs, so their URLs are public. Forgeable payloads cost nothing while the functions pointed at a dead repo — and would have cost plenty the moment they pointed at a live one. Reviewing exactly at the repoint meant the hardening shipped in the same deploy as the risk.
Run the pipe before you trust the pipe
The corrected code sitting unreleased on main — written specifically to fix this loop — carried its own wrong channel ID, and my assumed blocker list was wrong until a live test found the real one: a stale credential. Static review told a comforting story. One real row through the real pipe told the true one.
Keep the human gate boring and absolute
Every PR merges through me. Every credential rotates through me. Every schema and edge-function deploy happens with me in the loop. None of that limits the agent — it's the reason I can let the agent run fast everywhere else.
FAQ#
Did version one actually fail?
The loop didn't fail — it was orphaned. Version one worked: athletes filed real bug reports from inside the app, 37 of them flowed through the intake pipe (I still have every row, preserved in a backup schema in the current database), and issue numbers in the old repo climbed past #150. What failed was operational: when I retired that repo and rebuilt the app as v3, every deployed piece of the loop kept running against the dead repo, and nothing forced me to notice. The design was validated; the operations weren't. That distinction is why round two started with an audit instead of new features.
What does Claude do, and what do you do?
Claude Code writes code on claude/* branches — it has co-authored 44 of the 82 commits on main — writes the failing test before the fix, and opens PRs labeled needs-joe. I merge every PR personally; the agent has never merged its own work. I also hold the keys: GitHub credentials, Slack tokens, and webhook secrets are set and rotated by hand, and the database side — schema, edge functions — is a gated path that only deploys with me in the loop. Two of the thirteen merged PRs so far came off Claude branches, both merged by me on September 1. The speed comes from the automation; the safety comes from the gate.
Is a keyword heuristic really enough to classify Slack messages as bugs?
It's a deliberately dumb classifier — a minimum message length plus a keyword list ("crash", "broken", "can't", and friends) — and it will misjudge messages at the margins. For a small TestFlight beta that's the right trade: a false positive costs a label change during triage, and a missed report is still sitting in a channel where a human reads it. The in-app path doesn't use the heuristic at all — a structured form submission is already a bug report by definition, and that path produced all 37 of v1's real reports. If the Slack path starts misfiring on real traffic, the classifier is one function in one file, and there's a triage layer downstream specifically to catch what it gets wrong.
Nothing bad happened while the endpoints were forgeable. Why does the security fix matter?
Because the only thing protecting them was irrelevance. These functions run with JWT verification off so that database webhooks and Slack's Events API can reach them — which makes their URLs publicly callable by anyone. For months, a forged payload could have opened arbitrary GitHub issues or posted into the team's channels. It cost nothing because the deployed functions pointed at a retired repo nobody cared to abuse. Reconnecting them to a live repo and a real team without verification would have turned a harmless zombie into a spam cannon. The redeployed versions fail closed: the webhook path requires a shared secret, the Slack path verifies Slack's HMAC request signature with a replay window and a constant-time comparison, and an unset secret means every request is refused.
What happens after the credential is rotated?
The live test already answered most of that: a bug-report row fires the webhook, the secret check passes, the function builds the GitHub issue payload — and halts at authentication because the stored credential is stale. Rotation is a hand-done step on my side. Once it lands, the same insert produces a labeled issue in the v3 repo and a Slack post linking to it, and the type-a-bug-into-Slack path goes live with it. What that does not finish is the agent side of the loop: no scheduled job of mine delivers to Slack yet, the triage sweep hasn't run for this project, and the OTA workflow still ships silently. Those are the next bricks, and I'd rather list them here than pretend the loop is done.
Why Slack this time, when your other loops run on Telegram?
Audience. My personal loops report to Telegram because I'm their only consumer. This loop's consumers are a coach and his athletes, and the reporting surface has to live where they already talk. The workspace was purpose-built for the beta back in January — an intake channel with a pinned bug-report template, a feature-request channel, a dev channel, and a bot user — months before the loop could do anything useful with it. Honestly, that ordering is a mistake I'd repeat: the empty rooms were embarrassing in the audit, but the structure meant the relight only had to fix code, not org design.
Related Systems#
The method behind this loop didn't start here. The Hermes case study covers the agent runtime it grew out of; the Self-Healing Chatbot applies the same closed-loop philosophy to LLMOps.
Ask
Open the chat and ask how the loop is wired, what the audit caught, or what's still missing — the honest answer is the interesting one.
