Iteration loop architecture
How PinAppAI v2 organizes review rounds: atomic iterations with frozen manifests, an event-sourced state machine, drift detection, and a dashboard built around what's pending vs. what's been resolved.
PinAppAI’s review backbone is iteration-first as of v2: a project’s change requests don’t just have an admin status, they belong to iterations — discrete rounds of review with a frozen manifest, a reviewer roster snapshot, and an event log that traces every state change. This page documents the model so you can read a project’s history accurately, drive iterations from MCP, or build integrations on top of the new endpoints.
If you’ve used the legacy status-first dashboard (Open / Processing / Done / Wontfix chips), the new model maps onto it cleanly and runs alongside it during the migration window.
The loop
[setup-project] ← install the widget + reviewers
│
▼
reviewers leave change requests on the live site
│
▼
admin triages → schedules → opens iteration N ← manifest frozen here
│ (CR ids + reviewer roster)
▼
AI agent applies the bundle ← /pinappai:apply-decisions
│ state moves scheduled → applied
▼
reviewers decide on each CR ← state moves applied → in_review
│
▼
admin acknowledges per CR
│
├── accept → closed_accepted (terminal)
│
└── reopen → scheduled (loop back; iterations_traversed += 1)
│
▼
[iteration N+1] picks it up
The loop terminates per CR when the admin acknowledges with accept (or sets wontfix directly, which is also terminal). Iterations themselves don’t “close” — they’re just applied; whether their CRs continue to circulate is a per-CR question.
What an iteration captures
Each iteration row holds:
seq_no— 1, 2, 3, … per projectopened_at/applied_at— wall-clock timestamps;applied_at IS NULLmeans openmanifest_cr_ids_json— the frozen list of CR ids included at open timemanifest_reviewer_ids_json— the reviewer roster as of open time (snapshot)bundle_summary— short text added at apply time, surfaced in Historysource—admin,mcp, orbackfill(synthesized from legacy data)
The manifest freezes at open: adding a reviewer or another CR after that moment doesn’t retroactively change iteration N’s roster — they show up in iteration N+1 instead. This is what makes “the 14 rejected ones from this round” answerable as a stable query.
State machine
Each change request carries a cached state column whose value is the fold of its event log:
triage → scheduled → applied → in_review →
↑ ├── accept → closed_accepted (terminal)
│ └── reopen → scheduled (loops)
wontfix ←───┘ (terminal — no re-entry from in_review)
The cache exists because folding events on every dashboard read would be too slow on D1 above a few hundred CRs per project. A nightly drift-detection cron walks every CR, re-derives state from events, and logs any mismatch to cr_state_drift_log — this is what keeps the cache honest and surfaces missed dual-write hooks before they snowball.
Coverage — the headline metric
For an iteration with N CRs in its manifest:
items_decided= how many of the N have at least onereviewer_decidedeventitems_undecided= N − items_decideddisagreement_count= how many of the N have conflicting decisions across reviewersreviewers[]= per-reviewer count of decided items
The “headline coverage” is items_decided / manifest_size — that’s what the admin dashboard’s coverage bar visualizes and what pinappai_get_iteration_coverage returns to your AI agent. Per-reviewer breakdown is diagnostic (“yigit 30/50, maya 20/50, tom 0/50, 80% items touched”).
Driving iterations from MCP
@pinappai/[email protected] adds 5 tools that map 1:1 to the atomic primitives:
pinappai_open_iteration— opens iteration N+1 with a frozen manifestpinappai_mark_iteration_applied— moves manifest CRs to appliedpinappai_acknowledge_change_request— per-CR accept/reopenpinappai_get_iteration_coverage— read-only coverage snapshotpinappai_list_iterations— paginated history
A typical AI-driven loop looks like:
# Inside Claude Code or any MCP-aware agent:
/pinappai:apply-decisions # uses pinappai_open_iteration internally,
# walks the manifest, edits source,
# then calls pinappai_mark_iteration_applied
The slash command writes the .pinappai/last-applied.json marker as before — that file is now a 1-line cache pointing at the server-side iteration id, not the source of truth. Older @pinappai/mcp versions (≤ 0.4.0) keep working against the legacy compatibility shim through a 6-week deprecation window.
When to pick which surface
| Surface | Best for |
|---|---|
app.pinappai.com/dashboard-v2 | Manual review, coverage glance, ad-hoc per-CR acknowledge |
@pinappai/mcp from Claude Code / Cursor | AI-driven apply + acknowledge, large iterations, programmatic workflows |
| Legacy status-first dashboard | Existing muscle memory; still fully supported through the migration window |
The new dashboard is opt-in via the topbar link until your workspace’s dashboard_version flag is flipped to 'new' — at which point the project’s default landing page becomes iteration-first. Both layouts read the same underlying data, so flipping back and forth is non-destructive.
Hand-off into the build
Implementation lives in:
- API:
apps/api/src/lib/iterations.ts(atomic primitives),apps/api/src/routes/admin-iterations.ts(session auth),apps/api/src/routes/me-iterations.ts(API key auth) - Schema: migrations 0030–0034 (
iterations,change_requests,change_request_events,workspaces.dashboard_version,cr_state_drift_log) - Drift cron:
apps/api/src/lib/cr-state-drift.tsruns daily at 03:00 UTC - MCP tools:
apps/mcp/src/tools/{open-iteration,mark-iteration-applied,acknowledge-change-request,get-iteration-coverage,list-iterations}.ts
For the design rationale and trade-off discussion that drove this shape, see the design plan: docs/superpowers/plans/2026-05-07-iteration-loop-v2.md.