The Closed Loop
How a report travels from a user's device, through a coding agent, into a build — and back to the same user to confirm the fix. Design rationale lives in DESIGN.md §7; this page is the contributor's map.
The flow
SDK report (+ reporter_id, notify_reporter) → dashboard / MCP → agent: claim · ask · fix · after-shot
↑ ↓ PR description "FeedbackKit: <id>"
reporter's device ← reporter-updates ← feedbackkit release ← merged (github-webhook)
"is it fixed?" → verified | still broken (+ new screenshot) → reopened → re-dispatched
(only if the reporter opted in; otherwise the team's "Mark verified")Starting the agent
Five routes, all ending in the same linking/shipping/verifying steps. User-facing walkthrough: the dashboard's docs page Hand reports to an agent (web/src/pages/docs/DocsAgentsPage.tsx, mirrored as the agents topic in cli/src/docs.ts).
| Route | Trigger | Code |
|---|---|---|
| MCP (you + your agent) | The agent calls get_prompt / claim_feedback | cli/src/mcp/server.ts, loopInstructions in cli/src/loop.ts |
| GitHub-hosted Actions | Send to agent → issue + projects.dispatch_labels / dispatch_comment | create-github-issue, dispatchIssueToAgent in _shared/github.ts; the repo's own workflow (claude-code-action needs allowed_bots: feedbackkit-app) |
| Self-hosted Mac runner | Same labels, runs-on: [self-hosted, macOS, feedbackkit-agent] | skills/setup/setup-agent-runner/ (Claude Code and Antigravity templates) |
| GitHub Copilot | projects.dispatch_copilot → assign copilot-swe-agent[bot] with a member's user token | 0018_copilot_dispatch.sql, github-user-auth, assignIssueToCopilot |
feedbackkit watch | Run on my machine inserts a dispatched event with data.target = "local" | cli/src/commands/watch.ts (claims with data.queue_event) |
On a reopen, reporter-updates re-applies the labels and re-assigns Copilot as the last member who dispatched it.
Delivery modes (0024)
projects.delivery_mode: batch (merge → a beta ships every merged fix → verified → promote) or branch (PR → a preview build ships the PR's fixes via release --channel preview --pr <n> → verified → the PR's "FeedbackKit" commit status passes → merge). The status is computed by prVerification() in supabase/functions/_shared/prStatus.ts (pending / success / failure from the linked reports' stages) and posted on the PR's current head by syncPrStatus, called from github-webhook, ci-release, reporter-updates and pr-status. Agent instructions switch on the mode in three places (see Keep in sync). User docs: /docs/delivery; setup: the setup-release-loop skill asks which mode first.
Reporter opt-in (0023)
feedback_items.notify_reporter is the composer's "Notify Me When It's Fixed" (off by default; null = an SDK from before the option, treated as opted in). reporter-updates skips reports with false; release_readiness counts their shipped fixes as unreachable instead of awaiting, so they don't hold a build back; the dashboard and Portal offer Mark verified (a verified event with actor_type = 'user', which 0022 keeps from being announced as the reporter's).
Push-to-main and betas
Agents commit to main with a FeedbackKit: <id> trailer. github-webhook's push handler moves the report to merged. beta.yml runs the tests, ships the iOS Portal to TestFlight and the macOS Portal as the beta-portal-mac prerelease, and scripts/feedbackkit_announce.sh records each build through ci-release with the project's release token. The owner promotes a verified beta (Releases tab, feedbackkit promote). See DESIGN.md §8.
| Piece | Path |
|---|---|
| Release tokens, channels, readiness view | supabase/migrations/0015_push_to_main_releases.sql |
| Access tokens (scopes, run tokens), after-fix previews, retention | supabase/migrations/0025_access_tokens_and_previews.sql, attach-preview, cleanup-previews |
| Token-authenticated release API | supabase/functions/ci-release/ |
| Push trailer linking | supabase/functions/github-webhook/ (handlePush) |
| Beta pipeline | .github/workflows/beta.yml, scripts/feedbackkit_announce.sh, .github/actions/setup-feedbackkit-cli |
| Readiness UI | web/src/components/ReleasesPanel.tsx, ReleaseTokensCard.tsx |
Fix stages
feedback_items.fix_stage (checked text, 0014_closed_loop.sql) — kept separate from status because shipped Portal builds decode status strictly.
fix_stage | Set by | status becomes |
|---|---|---|
agent_working | MCP claim_feedback, dispatch in create-github-issue | in_progress |
pr_open | github-webhook (PR opened), MCP/CLI link_fix | in_progress |
merged | github-webhook (PR merged, or a trailer on a push to main), link_fix with a commit | in_progress |
shipped | record_release RPC via feedbackkit release | in_progress |
verified | reporter-updates (reporter taps "Yes, it's fixed"), or Mark verified in FixLoopPanel | resolved |
reopened | reporter-updates (reporter taps "Still broken") | in_progress |
wont_fix is never overridden.
Where each piece lives
| Piece | Path |
|---|---|
Schema, RLS, compare_builds, record_release | supabase/migrations/0014_closed_loop.sql |
Reporter-facing API (anonymous, project_key + reporter_id) | supabase/functions/reporter-updates/ |
| PR tracking, signature verification | supabase/functions/github-webhook/ |
| Agent dispatch (labels / trigger comment) | supabase/functions/_shared/github.ts dispatchIssueToAgent |
Agent MCP tools, release/link/timeline | cli/src/loop.ts, cli/src/mcp/server.ts, cli/src/commands/ |
| Swift SDK: identity, transport, card | Model/FeedbackReporterIdentity.swift, Networking/FixUpdatesClient.swift, UI/FixVerification*.swift |
| Web SDK: identity, transport, card | web-sdk/src/fixes.ts, web-sdk/src/ui/fixCard.ts |
| Dashboard | web/src/components/FixLoopPanel.tsx, AgentDispatchCard.tsx, LoopChecklist.tsx, WatchButton.tsx; checklist and stuck-report rules in web/src/lib/loopHealth.ts |
| Watchlist | feedback_watchers + notify_watchers() (0023); Portal toolbar in FeedbackDetailView.swift |
| Portal | DeveloperApp/Sources/Views/Detail/FixLoopSectionView.swift |
| Agent Skills | skills/workflow/fix-feedback/, skills/setup/setup-release-loop/, skills/setup/setup-agent-runner/ |
Keep in sync by hand
- Build ordering —
compare_buildsin SQL,_shared/builds.ts,cli/src/loop.ts,FeedbackBuild.compare(Swift),compareBuilds(web SDK). - reporter-updates wire format — the Edge Function,
FixUpdatesClient.swift,web-sdk/src/fixes.ts. - Event kinds — the
feedback_events.kindcheck constraint,FeedbackEventKindinweb/src/lib/types.tsandcli/src/types.ts, labels inFixLoopPanel.tsxandPortalFeedbackEvent.title, and the watcher titles innotify_watchers(). - Notification kinds — the
notifications.kindcheck,NotificationKindand its label/hint/dot maps inweb/src/, andPortalNotification.iconName/ the Portal's row tint. - Reporter reachability —
reporterReachable()inweb/src/lib/loopHealth.ts,PortalFeedbackItem.canReachReporter,ask_reporter's message, and therelease_readinessview. - The agents page —
DocsAgentsPage.tsxand theagentstopic incli/src/docs.ts. - Delivery-mode agent instructions —
closingTheLoopSection(web),loopInstructions(CLI/MCP), and the create-github-issue body; and the delivery docs page vs. thedeliverytopic incli/src/docs.ts.
Security notes
feedback_eventsinsert policy: members only,actor_type ∈ {user, agent},actor_user_id = auth.uid(). Reporter/GitHub/system events are written only by Edge Functions with the service role. No update/delete policy — it's an audit trail.reporter-updatesonly ever touches rows matching both the project key and the reporter id, rate-limits reporter writes per project, and honoursallowed_origins.github-webhookrejects every delivery unlessGITHUB_WEBHOOK_SECRETis set and the HMAC matches, and scopes matches to the delivery'srepository.full_name.
Testing
supabase start && supabase db reset
echo GITHUB_WEBHOOK_SECRET=testsecret > /tmp/fk.env
supabase functions serve --env-file /tmp/fk.env # another terminal
cd cli && npm run build && node --test test/closed-loop.integration.mjsPlus unit tests in each package (swift test --filter FixVerificationTests, web-sdk npm test + npm run test:e2e, cli npm test, Portal PortalTests).