| name | red-team-review |
| description | Adversarial bug-hunting code review of the apps/ monorepo — apps/web, @iep/domain, @iep/data, @iep/services, @iep/auth. Hunts real bugs in state-machine guards, rights derivation, Drizzle queries, server actions, persona/audit emission, and idempotent seed/migration code. Not style nits. Runs inline in the current chat (no subagent fork). Use when the user asks for a "red team", "bug hunt", "adversarial review", or at checkpoints during long lifts. |
Red-team adversary review (inline)
Perform this review inline in the current chat using the session's own Read, Grep, and Bash tools. Do NOT spawn an Agent — the goal is to use the active session, not to fork a separate run.
Scope is the consolidated app: apps/web and packages/{ui,domain,data,services,auth}. Prototypes in prototypes/ are out of scope (they're a source-of-lift, not a thing we ship). The development harness at apps/control-room is out of scope too — it isn't shipped.
When to invoke
- User says: "red team", "bug hunt", "adversarial review", "find what's broken", "find real bugs"
- After a non-trivial change to a state machine, rights derivation, server action, or Drizzle query
- After a prototype lift, before its commit lands
- Before a demo or external review
Setup — read these first
Scope to the change set. If none was named, default to the load-bearing surfaces:
packages/domain/src/machines/ard/ — ARD lifecycle machine, guards (quorumGate, recordingGate, recessAllowed, amendmentInScope), replayARDLifecycle.
packages/domain/src/rights/ — deriveVisibility, canPerform, role-grant time windows, edge filtering.
packages/data/src/queries/*.ts — every Drizzle query. Pay special attention to where(...) clauses (boundary conditions, null handling, time windows) and inArray(...) with possibly-empty inputs.
packages/data/src/schema/*.ts — column constraints, FK cascade rules, jsonb shapes.
apps/web/src/app/**/actions.ts and apps/web/src/app/**/route.ts — server actions and route handlers (auth, validation, revalidation, audit emission).
apps/web/src/lib/composition.ts (thin re-export) and packages/services/src/composition.ts — composition root. Wiring mistakes here propagate.
apps/web/src/lib/persona.ts + packages/auth/src/persona-token.ts — persona resolution, signed cookies.
- Anything the user names as recently changed.
Skim, don't deep-read — the goal is to spot bugs, not summarize files.
What to hunt — adapt to the change set
Find real bugs, correctness issues, and abuse risks — not style nits. Rank findings CRITICAL / HIGH / MEDIUM / LOW.
-
State-machine replay correctness. replayARDLifecycle rebuilds state from audit_logs. Are initialContext flags (activePhysicalDanger, expellableOffense, requiredRoles) supplied at every call site, or do default values silently change carve-out behavior? Does a missing actorPersonId (null in the audit row) get coerced consistently? Are at timestamps monotonic, or can clock skew put events out of order on replay?
-
Guard predicates. In machine.ts, quorumGate counts excused-with-input as covered. Does it still pass when requiredRoles is empty (vacuous quorum) — is that the desired behavior for the seed-stage IEP, or a leak? Does recordingGate block on the six-key special-factors checklist when specialFactorsConsidered is null vs. an empty object? Does amendmentInScope accept a scopeDiff with zero fields (no-op amendment = "in scope" trivially)?
-
Rights derivation. deriveVisibility walks graph edges. Are role-grant start/end windows applied consistently between loadActiveEdges (which filters) and canPerform (which may or may not)? An expired CASE_MANAGES edge that still appears in a stale snapshot lets a former case manager read a child's plan. Does canSeeStudent short-circuit administrators correctly without also bypassing time-window checks?
-
TOCTOU in server actions. Does any actions.ts read state, compute, and write in two separate round-trips without a transaction? Concurrent clicks (or duplicate-fire from stale useTransition state) can replay the read and double-insert. Look for "count then insert" or "select then update" patterns missing db.transaction(...) or SELECT FOR UPDATE.
-
Audit emission gaps. Every state transition should emit to audit_logs. Check that server actions wrap the data write + audit emit in the same tx so an audit failure rolls back the data write (and vice versa). A naked db.insert(...) followed by services.audit.emit(...) is two transactions.
-
Drizzle correctness. inArray(col, ids) with ids = [] is undefined-behavior territory — confirm callers guard. eq(col, value) where value could be undefined silently becomes WHERE col IS NULL in some Drizzle versions. LEFT JOIN chains with nullable cols on the right — confirm SELECT returns expected nulls vs. omitting rows. orderBy(col) without a tiebreaker on a non-unique key gives nondeterministic pagination.
-
Persona / auth boundary. getCurrentPersona() reads a signed cookie. Can an unauthenticated request reach a server action that mutates? Server actions don't get the Next.js middleware treatment automatically — confirm each one calls requirePersonaKind(...) or similar. IEP_PERSONA_SECRET must be ≥32 chars (composition root throws); check no test fixture leaks a short secret.
-
Idempotent seed. seed-personas.ts early-returns on existing admin user. Does seedScenarios (called from the same tx) survive a partial prior run — student exists but iep/version/meeting/audit chain doesn't? The onConflictDoNothing on the student row early-returns the whole iteration; an incomplete chain stays incomplete forever.
-
Resource cascade. FKs with onDelete: "cascade" are blast-radius traps. Deleting a students row cascades into ieps → iep_versions → meetings → audit_logs. Is there any admin surface that lets a user delete a student? If yes, is the cascade intentional, gated, or accidental?
-
Server-only boundary. import "server-only" is the seatbelt. Confirm every composition.ts, every actions.ts, every route.ts either has it or imports a module that does. A client component that accidentally pulls in @iep/data exposes DATABASE_URL to the browser at compile time.
-
Migration / schema drift. packages/data/drizzle/ has hand-checked SQL files. After a schema edit, did db:generate run and a new migration land? A schema-pgTable change without a generated migration is a deploy-time time bomb.
-
Visibility leaks via composition. services.rights.deriveVisibility(persona, edges) is the choke point. Are pages calling it and using its result, or fetching with raw studentQueries and trusting the URL? A page that does getStudent(id) without checking scope.studentIds.has(id) is a horizontal privilege bypass.
Walk the list. Skip what doesn't apply. Add branches that fit the specific area touched.
Output format
Inline in the conversation, ~300–500 words. Group by severity. For each finding:
file:line reference (markdown link form [name](path#Lline) for VSCode click-through).
- one-sentence description of the bug.
- one-sentence trigger / exploit condition.
Do NOT propose fixes — just the bug. End with a one-sentence delta vs. the last red-team if one exists. Be terse. Be specific.
Cleanup
You are the main session — don't create scratch files. Output findings inline. End with a git status check; nothing new should be staged.