Skip to content

feat(audit): persistent return-loop with auth, reminders, inline re-audit#397

Open
SiddarthAA wants to merge 13 commits into
FailproofAI:mainfrom
SiddarthAA:stable
Open

feat(audit): persistent return-loop with auth, reminders, inline re-audit#397
SiddarthAA wants to merge 13 commits into
FailproofAI:mainfrom
SiddarthAA:stable

Conversation

@SiddarthAA
Copy link
Copy Markdown

@SiddarthAA SiddarthAA commented Jun 1, 2026

Summary

Build out the end-to-end "come back better" return-audit loop on the
/audit results page so a freshly-audited user has a single, persistent
place to (a) verify themselves, (b) schedule the 7-day re-audit, and
(c) re-run the scan inline without leaving the page. The loop persists
across reloads via the same ~/.failproofai/*.json files the CLI uses,
so the desktop UI and the CLI share one source of truth.

This collapses what used to be three disjoint affordances — a header
sign-in pill, an "install policies" CTA, and the static
"recommended in 7d" copy — into one stateful section (section 06 — NEXT AUDIT) that reflects the real lifecycle of a returning agent.

What changed

Auth + reminder backbone (lib/auth/auth-store.ts, app/api/auth/*)

  • Added a thin file-backed auth store at ~/.failproofai/auth.json that the
    Next.js routes share with the CLI. Sessions are validated server-side
    on every probe; no client-side trust.
  • New endpoints:
    • GET /api/auth/status — returns {authenticated, user, reminder}
      in a single round-trip; reminder is hydrated from
      ~/.failproofai/next-audit.json when the active session owns it.
    • POST /api/auth/reminder — persists {next_audit_at, user_email, set_at}; idempotent, takes {in_days} (default 7).
    • POST /api/auth/login (used by the dialog) — writes auth.json
      after the verification step.
  • All three are no-store and never leak data across sessions: the
    reminder is only returned when the requesting cookie maps to the
    user_email recorded in next-audit.json.

Return section UI (app/audit/_components/return-section.tsx)

Three-state machine, driven by a single /api/auth/status probe on
mount:

State UI
unknown (probe in flight) buttons disabled
anon [ set a reminder ] opens AuthDialog; [ re-audit now ] + [ install policies ] remain available
authed consolidated status panel — "signed in as …" + either "next audit in N days" (when persisted) or "no reminder set yet" with an inline [ set a reminder ]

Key behaviors:

  • Single-click reminder. When an anon user clicks [ set a reminder ],
    the dialog auto-persists the 7-day reminder on successful auth — no
    second click required.
  • Persistent across reloads. The panel re-hydrates from
    next-audit.json on every mount; refreshing the page does not reset
    the loop.
  • Consistent post-auth UI. Once authed, the section stays in the
    status-panel layout permanently — it no longer falls back to the
    anonymous CTA strip when the reminder field is briefly null (e.g.
    during a reminder POST or after a manual clear). This fixes the
    reported regression where the section appeared to "go back to the old
    one" after successful auth.

Inline re-audit (app/audit/_components/rerun-button.tsx)

  • Extracted triggerRun({cli, since}) so both the empty-state CTA and
    the in-flow [ re-audit now ] button share one code path.
  • Re-audit hits POST /api/audit/run with the same since: "30d"
    default the dashboard uses, then window.location.reload()s to
    re-hydrate the cached result and dashboard with the new scan.
  • Button is busy-state aware ([ scanning… ]) and is no-op while a run
    is in flight.

Auth dialog (app/audit/_components/auth-dialog.tsx)

  • New shared modal: headline + reason are caller-supplied so the same
    component can be reused for any verification gate (today only the
    reminder flow uses it, but the API is open for the dashboard and
    share-card flows).
  • Closes on successful verification and emits the canonical
    {id, email} user shape upstream.

Styling (app/audit/audit-styles.css)

  • New .return-status panel + .rs-row / .rs-dot-pink /
    .rs-dot-green / .rs-email / .rs-strong primitives matching the
    existing terminal aesthetic (pink primary line, green confirmation
    line, subtle grid background tinted with --accent-pink).
  • Retained .auth-status-pill for non-section contexts.
  • Type scale bumped on the section heading and the primary
    reminder line for legibility at desktop widths.

Docs

  • Documented ~/.failproofai/next-audit.json and the
    /api/auth/reminder contract alongside the existing
    ~/.failproofai/auth.json notes so both the CLI and the web stay
    honest about the shared store.

Files touched

  • app/audit/_components/return-section.tsx (new)
  • app/audit/_components/auth-dialog.tsx (new)
  • app/audit/_components/rerun-button.tsx (new — extracted)
  • app/api/auth/status/route.ts (new)
  • app/api/auth/reminder/route.ts (new)
  • lib/auth/auth-store.ts (new)
  • app/audit/audit-styles.css (extended)
  • app/globals.css (minor token tweaks)

Test plan

  • Anon load → section shows [ set a reminder ] [ re-audit now ] [ install policies ]; no email pill.
  • Anon → click [ set a reminder ] → AuthDialog opens; verify → panel flips to status layout with "in 7 days" + signed-in line, single click.
  • Authed-no-reminder load → panel shows "no reminder set yet · recommended in 7 days" + inline [ set a reminder ]; clicking persists without re-opening the dialog.
  • Authed-with-reminder reload → panel re-hydrates from ~/.failproofai/next-audit.json; days counter is correct (1 day → "in 1 day", >1 → "in N days").
  • [ re-audit now ] from either layout triggers a scan and reloads with fresh results; double-click is debounced.
  • [ install policies ] only renders when there are unenabled builtins with hits.
  • bun run test:run green; bun run lint + bunx tsc --noEmit clean.
  • Manual: confirm sign-out (CLI failproofai logout) flips the web UI back to the anonymous CTA on next reload.
  • Manual: cross-account safety — auth as user A, set reminder, switch session to user B → user B sees authed-no-reminder, never user A's reminder.

Summary by CodeRabbit

  • New Features

    • Email OTP auth with CLI commands (login/logout/whoami) and in-app sign-in dialog.
    • New /audit dashboard: archetype reports, scoring/grades, findings, policy recommendations, and PNG poster export.
    • Triggered reruns, run/status polling, cached results, and persistent “next audit” reminders.
  • UI/UX

    • Design polish and unified styles across Audit, Policies, and Projects; improved empty/run states and share/score UX.
  • Documentation

    • CLI auth, env vars, and dashboard/re-audit guidance added.

SiddarthAA and others added 12 commits May 28, 2026 00:37
… shareable poster

Adds an in-app audit report at /audit that turns the existing
`failproofai audit` data into a personality-driven dashboard. Every
detector / policy hit feeds a weighted classifier that lands the agent
in one of 8 archetypes; the report uses that to motivate enabling
unenabled builtin policies.

What's new

- Archetype catalog + classifier (`src/audit/archetypes.ts`): 8 archetypes
  (optimist, cowboy, explorer, goldfish, paranoid architect, precision
  builder, hammer, ghost) with pixel sigils, taglines, "common in" /
  "primary risk" copy. `SIGNAL_MAP` maps every builtin policy + every
  audit-only detector (47/47 coverage) to an archetype with a tuned
  weight. Classifier picks the dominant archetype, falls back to
  `goldfish` for broad-spread agents, and to `precision` when no signal
  fired.
- Scoring (`src/audit/scoring.ts`): starts at 100, subtracts capped
  per-source penalties (deny -1.2, instruct/warn -0.7, sanitize -0.4,
  detector -0.5). Grade thresholds S/A/B/C/D/F match the reference.
  `projectedScore` previews the post-enable uplift; `syntheticRank`
  produces a stable cohort rank from the score.
- Derivations (`src/audit/strengths.ts`, `src/audit/findings.ts`):
  Strengths surface real numbers (clean-call %, avg turns, "0 credential
  leaks" when sanitize policies didn't fire, etc.). Findings carry
  hand-curated body + cost copy per policy slug and the real captured
  evidence from `AuditCount.examples`.
- Detector → policy fix mapping (`findings.ts:DETECTOR_TO_POLICY`):
  each of the 8 audit-only detectors is paired with the closest real-time
  builtin policy, so every finding card shows a real
  `$ failproof policy add <slug>` install command — no "audit-only"
  framing in the report. Multi-policy mappings render an "also covered
  by <policy>" hint. Prescribed-policy section aggregates detector hits
  into the target policy with `(via redundant-cd-cwd, …)` attribution.

Sections

01 Identity (archetype hero with sigil + meta grid), 01b Show off CTA,
02 Strengths, 03 Score + cohort leaderboard with distribution histogram,
04 Findings (per-policy cards: what happened / cost / evidence / fix),
05 Prescribed policies (with projected score uplift callout), 06 Return
loop ("re-audit in 7 days"). Server page reads the dashboard cache only;
all derivation is client-side. Catalog size is computed server-side and
passed as a prop (BUILTIN_POLICIES and audit detectors pull in node:fs
via the workflow / require-* policies, so they can't ship to the client).

Cache + API

- Dashboard cache at ~/.failproofai/audit-dashboard.json (mode 0600,
  single slot, new runs overwrite). Helper at
  `src/audit/dashboard-cache.ts` (read/write/staleness). Schema bumped
  to `version: 2` with new fields `eventsScanned: number` (total
  tool-use events scanned, drives the "X tool calls" headline),
  `projectsScanned: string[]` (drives the project filter), and
  `enabledBuiltinNames: string[]` (lets findings answer "is this fix
  already enabled?" without iterating result rows).
- POST /api/audit/run calls runAudit() in-process, writes the dashboard
  cache, and serializes via a module-scoped singleton lock so concurrent
  clicks 409. GET /api/audit/status reports {running, startedAt,
  cachedAt} for client polling. Server action
  `app/actions/get-audit-result.ts` reads the cache without triggering
  a run, mirroring the `/policies` `getHooksConfigAction()` pattern.

Re-run UX

- Empty state CTA on first visit; in-flight re-runs render a four-stage
  faux progress UI (`run-progress.tsx`). RerunButton POSTs `/api/audit/run`
  with the current scan params, polls `/api/audit/status` at 1Hz, and
  refetches via the server action when running flips false.
- Shareable PNG export: clicking "make poster" captures the identity
  archetype-frame DOM via html2canvas at scale 2 and downloads
  `failproofai-<archetype>-<YYYY-MM-DD>.png`. New dependency:
  html2canvas@^1.4.1.

Styling

- Ported `assets/audit/styles.css` (1235 lines) verbatim into
  `app/audit/audit-styles.css`, scoped to the route via page-level
  import. JetBrains Mono + VT323 loaded from Google Fonts; Architype
  Stedelijk shipped locally under public/audit/fonts/. Reference design
  kit (audit.jsx / poster.jsx / tweaks-panel.jsx / styles.css /
  archetypes.jsx + screenshots) committed under assets/audit/ for
  future iteration. ESLint config gains an assets/ ignore so the design
  kit's vanilla React-Babel JSX isn't linted as project source.

Animation primitives in app/globals.css: `.audit-row-enter` (staggered
fade-up via `--row-delay`) and `.audit-bar-fill` (width 0 → `--bar-width`
on mount), both honoring `prefers-reduced-motion`.

Navbar / layout

- Navbar gains an "Audit" entry between Policies and Projects with a
  ClipboardCheck icon and an optional slipping-count chip (rendered
  when the layout's server-side cache read finds >0 slipping hits).
  Layout passes the count via a new `auditSlippingCount` prop.

Core changes (additive, original policies untouched)

- `src/hooks/policy-registry.ts`: added `getAllPolicies()` and
  `setAllPolicies()` exports for snapshot/restore. Existing
  `registerPolicy` / `clearPolicies` / `getPoliciesForEvent` /
  `normalizePolicyName` semantics unchanged.
- `src/audit/replay.ts`: `initReplay()` now snapshots the registry via
  `getAllPolicies()` before clearing it; new `restoreReplay()` puts the
  pre-init policies back. `runAudit()` wraps the work in try/finally so
  embedding the audit in long-running processes (the Next.js dashboard
  is one) no longer wipes pre-existing registrations.
- `src/audit/index.ts`: surfaces `eventsScanned`, `projectsScanned`,
  `enabledBuiltinNames` on the result; per-transcript scan now tracks
  events count + cwd. Schema-version bump 1 → 2.

Tests

- New `__tests__/audit/dashboard-cache.test.ts` (round-trip, 0600 mode,
  corrupt-JSON resilience, staleness threshold).
- `__tests__/audit/replay.test.ts` adds three tests covering registry
  snapshot/restore: a user-registered policy survives `initReplay()` →
  `restoreReplay()`, `restoreReplay()` is idempotent, and calling
  `restoreReplay()` before `initReplay()` is a no-op.
- Full suite green: 1701 / 1701.

Verification

- `bunx tsc --noEmit` clean
- `bun run lint` 0 errors (2 pre-existing warnings retained)
- `bun run test:run` 1701 / 1701
- `bun --bun next build` succeeds; new routes `/audit`,
  `/api/audit/run`, `/api/audit/status` all registered
- Hook handler smoke against live config (`block-failproofai-commands`
  fires deny on `failproofai policies --uninstall`, harmless commands
  pass cleanly) — runtime policy enforcement intact
Adds the missing CHANGELOG entry for the /audit dashboard work and a
new "### Audit" section under docs/dashboard.mdx's Pages list. Also
appends `audit` to the FAILPROOFAI_DISABLE_PAGES valid-values list
(the page-level disable gate added in app/audit/page.tsx already honors
it; the docs were one step behind).

Translated dashboard.mdx mirrors (14 locales) are intentionally left for
the translation-sync workflow — same pattern as the env-vars docs from
0.0.11-beta.2.
- Expand every archetype in src/audit/archetypes.ts to a multi-variant
  catalog (4–6 taglines, keyword sets, descriptions, signature blocks,
  common-in / primary-risk / closing lines per archetype). A new
  pickArchetypeVariant(key, seed) resolver picks one variant per field
  via a djb2-hashed, per-field-axis index, so the persona blurb stays
  stable for a given project seed but two projects landing on the same
  archetype see different copy. IdentitySection consumes the resolved
  variant; the seed flows from audit-dashboard.tsx as the inferred
  project name. Fix the picker's signed-modulo bug (final XOR
  re-introduced signedness → negative index → undefined keywords)
  by forcing >>> 0 on the final mix.
- Simplify return-section's CTA to '[ install policies ]' copying the
  bare `failproofai policies --install` command (no per-policy short
  names appended).
- Fix the [ share → ] header button: replace scrollIntoView with a
  manual window.scrollTo that subtracts the sticky .app-header height
  (+16px breathing room), plus a scroll-margin-top: 80px fallback on
  .showoff.
- Harden the 'make poster' PNG export so the captured archetype frame
  no longer collides with the sigil / tagline: await document.fonts.ready
  before capture, apply a .capturing class that locks every clamp()'d
  font-size and grid column to an absolute value tuned for the 1100px
  capture width, drop text-shadow / box-shadow that html2canvas crops
  unpredictably, and capture with a 12px bleed on every side so the
  frame's corner accents survive the crop.
…pi-server

Implements end-to-end email-OTP auth against the Rust failproof-api-server,
exposed through both the `failproofai auth` CLI subcommand and the in-app
dashboard. Adds a gating step on the /audit page's "set a reminder" CTA so
unidentified visitors can verify themselves inline before reminders get
queued (mail-scheduling itself is deferred).

Architecture

  CLI (failproofai auth)            Dashboard (Next.js)
            \\                              /
             \\  reads/writes              /  reads/writes
              \\                          /
               ~/.failproofai/auth.json (mode 0600)
                          |
                          | bearer JWT
                          v
              failproof-api-server (Rust) -> Postgres

CLI surface (src/auth/cli.ts, dispatched from bin/failproofai.mjs)

  failproofai auth --login    Email + OTP flow, writes auth.json
  failproofai auth --logout   Revokes server-side, wipes auth.json
  failproofai auth --whoami   Prints identity from /me (silent refresh)
  failproofai auth --help     Usage

Readline input-masking is TTY-gated so piped stdin (tests / scripts) doesn't
stall on the per-character _writeToOutput callback.

Shared HTTP + persistence layer (lib/auth/)

  api-server-client.ts  Stateless fetch client. Endpoint helpers
                        (requestLoginCode, verifyLoginCode,
                        refreshAccessToken, logoutSession, fetchMe,
                        decodeJwt). AuthApiError carries status, code,
                        retry_after_secs. Base URL from FAILPROOF_API_URL
                        (default http://localhost:8080). Tolerates both
                        the documented {code,message} error shape and the
                        live server's {success,code,detail} shape.

  auth-store.ts         File persistence at ~/.failproofai/auth.json,
                        mode 0600 (creation + chmodSync on overwrite).
                        getValidAccessToken() auto-refreshes within a 60s
                        leeway; whoAmI() does one refresh-and-retry on a
                        hard 401 then wipes the file. FAILPROOFAI_AUTH_DIR
                        env-var override exists for tests.

Dashboard API routes (app/api/auth/)

  GET  /api/auth/status         {authenticated, user?} via whoAmI()
  POST /api/auth/login-request  Proxy; surfaces retry_after_secs
  POST /api/auth/login-verify   Proxy; on 200 persists tokens locally and
                                returns ONLY {authenticated, user} -- the
                                refresh token never reaches the browser
  POST /api/auth/logout         Revokes upstream, deletes auth.json
                                regardless of upstream success

Dashboard UI

  app/audit/_components/auth-dialog.tsx
      Modal dialog matched to /audit's pixel-craft aesthetic: pink corner
      brackets, dashed-frame backdrop, terminal mono inputs, masked OTP
      entry, live 30s resend countdown, ESC / backdrop / [x] close, error
      banner with rate-limit messaging.

  app/audit/_components/return-section.tsx
      Probes /api/auth/status on mount. [set a reminder] gates on auth:
      unknown -> button disabled, anon -> opens dialog with "oops -- you
      are unknown", authed -> flashes [reminder queued for <email>] and
      shows a green "signed in as <email>" pill under the CTA.

  app/audit/audit-styles.css
      New .auth-dialog* + .auth-status-pill rules using the existing color
      palette and font stack.

Production deploy hooks

  - Set FAILPROOF_API_URL on the user's machine OR change DEFAULT_API_BASE
    in lib/auth/api-server-client.ts to the prod URL before publishing.
    The npm package never touches Postgres directly -- only the HTTP
    surface of the api-server. Database / JWT / SES config all live with
    the api-server deployment.
  - CORS work is not needed: every browser-visible auth call goes through
    the Next.js API routes (server-side), so the api-server never sees a
    cross-origin browser request.
  - Refresh-token reuse detection happens on the api-server (rotated_to
    chain); the client treats any 401-from-refresh as "wipe local
    session" so theft-revoked users get pushed back to the login dialog.

Local dev loop

  1. docker run -d --rm --name failproof-pg -e POSTGRES_PASSWORD=postgres \\
       -e POSTGRES_USER=postgres -e POSTGRES_DB=failproof -p 5544:5432 \\
       postgres:16-alpine
  2. cd platform/failproofai/api-server && \\
     FAILPROOF_DATABASE_URL=postgres://postgres:postgres@localhost:5544/failproof \\
     FAILPROOF_JWT_SIGNING_KEY=<>=32-byte-string> \\
     FAILPROOF_BIND_ADDR=127.0.0.1:8080 \\
     FAILPROOF_EMAIL_SENDER_BACKEND=log FAILPROOF_ENVIRONMENT=local \\
     cargo run --bin server
  3. FAILPROOF_API_URL=http://127.0.0.1:8080 bun run dev
  4. In a fourth terminal:
       bun bin/failproofai.mjs auth --login
     OTP appears in the api-server's stdout under the
     "login code (dev log sender)" log line.

Verified end-to-end against a Docker postgres + the api-server: CLI login
+ whoami + logout, all four dashboard routes, the audit page rendering
with the gated reminder button, and the shared auth.json across both
surfaces (sign in via CLI -> dashboard sees it; logout via CLI ->
dashboard reverts to anonymous on next page load). Existing 1701 vitest
tests, eslint, and tsc all stay green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…I_URL

Documents the new `failproofai auth --login | --logout | --whoami` subcommand
and the two env-var knobs (`FAILPROOF_API_URL`, `FAILPROOFAI_AUTH_DIR`) shipped
with the auth feature. i18n mirrors will pick this up via the existing
translate-docs workflow on the next sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…yle leak

Promotes the /audit page's design language to the whole app so /policies and
/projects pick up the same fonts, palette, chrome, and component vocabulary.
Also fixes a latent bug where the audit page's :root + body resets persisted
on client-side navigation back to other routes, leaving them with audit's
JetBrains Mono / dark canvas but none of the matching section chrome.

Strategy: unify at the CSS-variable level. Every shadcn-style Tailwind token
(--background, --card, --foreground, --primary, --border, --radius, …) is
repointed at the audit palette (--bg, --bg-2, --ink, --accent-pink, …) in
globals.css. Existing Tailwind utility classes like `bg-card text-foreground
border-border` continue to work but now produce audit visuals — no component
rewrites needed for the 1661-line hooks-client tree.

Files changed

  app/globals.css        Rewritten. Single source of truth for fonts, tokens,
                         body atmosphere (cross-hatch + grain + pink vignette),
                         and every shared chrome class (.app-header / .h-brand
                         / .btn / .btn-press / .tabs / .tab / .section /
                         .section-mast / .section-h / .report / new .panel).

  app/audit/audit-styles.css
                         Trimmed by 150 lines. Drops :root, the html/body/#root
                         resets, the body atmosphere overlays, .app-header,
                         .btn, .tabs — all now live in globals. Keeps only the
                         /audit-only widgets (archetype-frame, sigil, score
                         grade, leaderboard, findings, return hook, auth
                         dialog). Side effect: nothing left to leak.

  app/layout.tsx         Removes the next/font/google Geist Mono import. Fonts
                         ship via the @import url(…JetBrains+Mono…) in
                         globals.css so the design system is one stylesheet.

  components/navbar.tsx  Rewritten around .app-header. Pink "▮▮" pixel mark +
                         Architype Stedelijk wordmark, optional version chip,
                         dynamic per-section eyebrow ("policies" / "audit" /
                         "projects"), .tab links with sharp pink underline on
                         the active route. Drops lucide icons from the bar.

  app/projects/page.tsx + loading.tsx
                         Wrapped in .report + .section + .panel. New
                         green-eyebrow masthead with the ━━ glyph and
                         "your agent footprint." section heading. Empty and
                         loaded states both use the dashed-frame .panel.
                         ProjectList component itself unchanged.

  app/policies/hooks-client.tsx
                         Top-level <div className="min-h-screen bg-background
                         …"> replaced with a .report + .section shell. New
                         masthead with audit-style copy ("what your agents
                         tried." / "what to stop them doing.") and an enabled-
                         count meta chip in pink. TabBar swapped from rounded
                         pill to global .tabs / .tab with sharp pink underline
                         on active. Dropped the unused ArrowLeft + back-to-
                         projects link (navbar handles cross-page nav now).
                         No inner refactor of ActivityTab / PoliciesTab.

Verification

  bunx tsc --noEmit         passes
  bun run lint              passes (only the 2 pre-existing warnings)
  bun run test:run          1701/1701 pass
  bun --bun next build      Compiled successfully in 6.2s; static + dynamic
                            routes for /, /policies, /projects, /audit, and all
                            /api/auth and /api/audit endpoints generated.

The user needs to restart `bun run dev` once after pulling this commit — the
Turbopack HMR pipeline can't hot-swap :root / @import changes reliably.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t now

Six polish items in one pass — sizing, second-navbar fix, score-section
rewrite, empty/running restyle, and persistent reminder state across
sessions.

Sizing
  globals.css: base 13 → 14.5px, .report max-width 1180 → 1380px (40px
  side padding), .section padding restored to 64px. Default-zoom
  readability across /audit, /policies, /projects no longer forces a
  browser zoom-in.

Double navbar
  Delete app/audit/_components/app-header.tsx and all three of its
  mount sites in audit-dashboard.tsx (cached, in-flight, and
  ShellEmpty). The global navbar already supplies brand + tabs + reach;
  the in-page bar with [share →] was redundant chrome.

Score section
  Drop the synthetic cohort leaderboard. Replace ScoreSection with a
  single .panel (.score-share-card) split into score + share:
    left  — big tier-colored score, tier badge, progress bar to the
            next grade band, 3 stat boxes (missing policies, pts to
            next tier, est. days to fix), policy-status chip strip
    right — X + LinkedIn pre-written templates derived from
            score/archetype/missing; [share on X], [share on
            LinkedIn], [download audit card] (html2canvas captures
            the entire panel as failproofai-card-<grade>-<score>.png)
  audit-dashboard.tsx drops the unused syntheticRank import / rank
  prop and threads `result` into the new section.

Empty / running
  empty-state.tsx: shadcn Button + lucide icon center card → .panel
  with a 6×6 pixel-grid sigil, Architype Stedelijk headline,
  .btn-press CTA, audit-style meta caption. Mode "no-cache" → "run
  your first audit." with [ run audit ]. Mode "zero-sessions" →
  "install hooks first." with [ install guide → ].
  run-progress.tsx: terminal-style panel — "$ failproofai audit
  --since 30d ▮" header with a blinking pink cursor, stage list with
  ✓ / ▮▮ / ○ markers + per-stage braille spinner, marquee progress
  bar with a pink shine sweep.

Persistent reminder
  ~/.failproofai/next-audit.json — separate from auth.json so a token
  refresh / re-login doesn't churn the reminder. Mode 0600, same
  perms hygiene as auth.json (writeFileSync with mode + post-write
  chmodSync on overwrite).
  lib/auth/auth-store.ts: new readReminder / writeReminder /
  deleteReminder / getReminderFilePath + StoredReminder type.
  app/api/auth/reminder/route.ts: GET / POST / DELETE. POST defaults
  to a 7-day offset; reminder is scoped to the active session so a
  reminder for a@x.com is invisible when b@x.com is the live CLI
  session.
  /api/auth/status returns `reminder: { next_audit_at, user_email,
  set_at } | null` alongside the user.

Return section
  Behavior matrix in return-section.tsx:
    unknown → buttons disabled while /api/auth/status is in flight
    anon    → [set a reminder] opens AuthDialog, on success persists
              the 7-day reminder automatically (no second click)
    authed + no reminder → [set a reminder] writes the timestamp
              directly, no dialog
    authed + reminder set → status panel showing
              "next audit set for <Mon Jun 8> · in 7 days" and
              "signed in as <email>", plus [re-audit now] /
              [install policies] / "clear reminder"
  [re-audit now] button is exposed to all authed states (plus anon,
  next to install-policies). It reuses triggerRun() from
  rerun-button.tsx and reloads the page once the new run finishes.

Verification
  bunx tsc --noEmit  passes
  bun run lint       passes (only the 2 pre-existing warnings)
  bun run test:run   1701/1701 pass
  bun --bun next build  Compiled successfully — new
    /api/auth/reminder route registers alongside /api/auth/{status,
    login-request, login-verify, logout}.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The new persistent re-audit reminder ships a small companion file
alongside auth.json. Add a short section to docs/cli/auth.mdx covering
its shape, the per-email scoping rule (so swapping CLI accounts hides
the previous user's reminder), the 0600 perms, and the GET / POST /
DELETE /api/auth/reminder endpoint that backs the UI button. CHANGELOG
Docs entry matched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an email OTP authentication system (CLI commands, local token persistence, refresh and revocation flows, Next.js proxy routes, and a per-user re-audit reminder) and a new /audit dashboard (server cache, run/status endpoints with concurrency guarding, archetype classification, scoring/grade/cohort, findings/policies UI, poster PNG export), plus design-system CSS, standalone audit assets, tests, and docs.

Changes

Email OTP Authentication & Audit Dashboard

Layer / File(s) Summary
Auth HTTP client and persistence libraries
lib/auth/api-server-client.ts, lib/auth/auth-store.ts
HTTP client for /v0/auth/* with AuthApiError parsing and JWT decoding; local auth and reminder persistence (auth.json, next-audit.json) with permission hardening and token refresh/whoami flows.
CLI authentication commands & CLI dispatch
src/auth/cli.ts, bin/failproofai.mjs
failproofai auth CLI with login/logout/whoami interactive flows, prompt masking, retries, and dispatch wiring; policy CLI subcommand added to top-level dispatcher.
Audit core computation
src/audit/archetypes.ts, src/audit/scoring.ts, src/audit/findings.ts, src/audit/strengths.ts
Archetype catalog and deterministic variant resolver, agent classification, scoring/grade/tier/cohort utilities, finding derivation and curated copy, and strengths derivation.
Audit run, replay, and types
src/audit/index.ts, src/audit/replay.ts, src/audit/types.ts
Run lifecycle changes: per-transcript cwd/events counts, runAudit inner/finally restoreReplay, bumped AuditResult schema to v2 (projectsScanned, eventsScanned, enabledBuiltinNames), and policy-registry snapshot/restore helpers.
Dashboard cache
src/audit/dashboard-cache.ts, __tests__/audit/dashboard-cache.test.ts
File-based cache ~/.failproofai/audit-dashboard.json with read/write helpers, permission tightening, staleness detection, and Vitest tests for read/write/stale/corrupt cases.
Audit API endpoints: run/status and run-state
app/api/audit/_state.ts, app/api/audit/run/route.ts, app/api/audit/status/route.ts
In-memory run state (tryAcquireRun/release), POST /api/audit/run with sanitization, concurrent-run 409 handling, runAudit invocation and cache persistence, and GET /api/audit/status polling endpoint.
Auth API proxy and reminder/status routes
app/api/auth/login-request/route.ts, app/api/auth/login-verify/route.ts, app/api/auth/logout/route.ts, app/api/auth/reminder/route.ts, app/api/auth/status/route.ts
Browser-facing proxies for OTP request/verify, logout/revocation, per-user reminder GET/POST/DELETE with validation, and lightweight auth status endpoint derived from local cache plus matched reminder.
Audit server page and server action
app/audit/page.tsx, app/actions/get-audit-result.ts, app/audit/loading.tsx
Server entry for /audit that reads cache, computes catalog size, conditionally notFound(), and renders client AuditDashboard with a Suspense fallback; server action to return cached result payload.
Audit dashboard React components
app/audit/_components/*
Client components: AuditDashboard orchestration, AuthDialog modal, EmptyState, RunProgress, Identity/Strengths/Score/Findings/Policies/Return sections, Sigil, ShowOffCTA, RerunButton, ReportFooter, and related helpers for capture/download/polling.
Design system and page styles
app/globals.css, app/audit/audit-styles.css
Centralized CSS tokens, font loading, shared app chrome classes (.app-shell, .report, .section, .panel, .tabs), and audit-specific styles (identity, strengths, score, findings, policies, auth dialog, poster capture).
Navigation, policies/projects UI updates
components/navbar.tsx, components/reach-developers.tsx, app/policies/hooks-client.tsx, app/projects/*
Navbar restyle with /audit link and slipping-count badge, ReachDevelopers dropdown color customization, and policies/projects pages migrated to new report/section chrome.
Standalone audit assets
assets/audit/*
Static HTML/JS/CSS audit report and poster pages (CDN React+Babel), archetype catalog, tweaks-panel, poster export styles and scripts for offline/standalone use.
Tests, docs, config, deps
__tests__/*, CHANGELOG.md, docs/*, eslint.config.mjs, package.json
Added tests for cache and replay; updated CHANGELOG; docs for CLI auth and env vars; ESLint ignore updated; added runtime dependency html2canvas.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

🐰 A dashboard takes flight,
Archetypes glow in the night,
Scores and tokens align,
Reminders set on time,
Auth, export, and insights — delight!

✨ Finishing Touches
⚔️ Resolve merge conflicts

❌ Error resolving conflicts.

  • Resolve merge conflict in branch stable

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/policies/hooks-client.tsx (1)

1573-1584: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the parent summary state in sync after configure-tab changes.

This effect seeds policyCounts and installedCliLabels once, but PoliciesTab can later install/remove CLIs and toggle policies. After those updates, the section meta and activity summary below keep showing the pre-change values until a full reload. Refresh these derived values from the same reload path you already use in PoliciesTab, or lift that summary state into a shared refresh function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/policies/hooks-client.tsx` around lines 1573 - 1584, The current
useEffect seeds policyCounts and installedCliLabels only once via
getHooksConfigAction(), so later changes in PoliciesTab are not reflected;
extract the fetch-and-set logic into a shared refresh function (e.g.,
fetchHooksConfig) that calls getHooksConfigAction() and then calls
setHooksInstalled, setPolicyCounts, and setInstalledCliLabels, update the
useEffect to call that function, and have PoliciesTab call the same
fetchHooksConfig after any CLI install/remove or policy toggle so the parent
summary state stays in sync.
🟠 Major comments (27)
app/api/auth/reminder/route.ts-94-96 (1)

94-96: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope reminder deletion to the active session.

DELETE unconditionally wipes next-audit.json. That means an anonymous caller, or a different signed-in user on the same host, can clear someone else's persisted reminder even though GET/POST are user-scoped.

Require whoAmI() here and only delete when the stored reminder belongs to that session; otherwise return 401/404.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/auth/reminder/route.ts` around lines 94 - 96, The DELETE handler
currently calls deleteReminder() unconditionally; change it to require whoAmI()
and scope deletion to the active session: call whoAmI() at the start of DELETE,
return 401 if it yields no authenticated session, fetch the stored reminder (the
same store used by GET/POST), verify the reminder’s owner/session id matches the
whoAmI() result, and only then call deleteReminder(); if no reminder exists or
it belongs to someone else return 404 (or 401 for unauthenticated), otherwise
return NextResponse.json({ ok: true }) after deletion. Use the existing function
names (DELETE, whoAmI, deleteReminder) to locate and update the logic.
src/auth/cli.ts-126-133 (1)

126-133: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't block --login on a stale auth.json.

readAuth() only proves the file exists. If the stored session is expired or already revoked, this early return forces the user to discover and run failproofai auth --logout before they can log back in.

Use whoAmI() or getValidAccessToken() before short-circuiting, and fall through to the login flow when the stored session can't be recovered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/cli.ts` around lines 126 - 133, The early return in runLogin() uses
readAuth() to detect an existing auth file but doesn't verify that the session
is still valid; update runLogin() to validate the stored session (call whoAmI()
or getValidAccessToken() using the data returned by readAuth()) and only
short-circuit when the token is confirmed valid; if validation fails or throws,
fall through to the interactive login flow and remove the current early-return
behavior so stale/revoked tokens do not block --login.
lib/auth/auth-store.ts-64-247 (1)

64-247: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Add regression tests for the new persistence and refresh flows.

This file adds disk validation, refresh-on-expiry, refresh-on-401, and cross-user reminder scoping, but there’s no accompanying __tests__ coverage in this review set. These paths are stateful and easy to regress.

Based on learnings/coding guidelines: "Always add unit tests for new behaviour. Place tests in tests/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/auth/auth-store.ts` around lines 64 - 247, You need to add unit tests
under __tests__ covering the new persistence and refresh behaviours: write tests
for readReminder/writeReminder/deleteReminder and readAuth/writeAuth/deleteAuth
to validate on-disk validation and permission handling (use mocks/fs temp dirs),
tests for authFromTokenResponse and readAccessExpiry (decodeJwt) to assert
token→StoredAuth conversion and exp parsing, tests for getValidAccessToken to
exercise successful no-refresh, refresh-on-expiry, and refresh failure paths
(mock refreshAccessToken and AuthApiError), and tests for whoAmI to cover normal
fetchMe, refresh-on-401 retry (mock readAuth/refreshAccessToken/fetchMe) and
unrecoverable 401 leading to deleteAuth; place these tests in __tests__ and use
time mocking (Date.now), fs mocks or temp filesystem, and mocking of
refreshAccessToken/fetchMe to simulate network and error cases.
app/api/auth/reminder/route.ts-62-67 (1)

62-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject malformed JSON instead of silently scheduling the default reminder.

req.json().catch(() => ({})) makes an invalid body behave like an empty one, so a bad client request still writes a 7-day reminder. This should be a 400, not a successful mutation.

Treat only an actually empty body as "use defaults"; malformed JSON should fail validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/auth/reminder/route.ts` around lines 62 - 67, Remove the silent
.catch that converts malformed JSON to {} and instead detect and reject invalid
JSON: read the raw request body via req.text(), if the text is empty or only
whitespace then set body = {} (use SetBody) to apply defaults; otherwise attempt
JSON.parse on the text (or call JSON.parse after text) and if parsing throws
return a 400 response. Update the code around the existing body/SetBody logic in
the route handler that currently does body = (await req.json().catch(() =>
({}))) as SetBody so malformed JSON results in a 400 while truly empty bodies
still use the default 7-day offset.
lib/auth/auth-store.ts-87-97 (1)

87-97: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the shared JSON writes atomic.

These files are intentionally shared between the CLI and the dashboard, but both write paths replace the target file in place. A concurrent write or process crash can leave truncated JSON behind, which turns into a silent logout/reminder loss on the next read.

Suggested direction
+import { renameSync } from "node:fs";
+
+function writeJsonAtomically(path: string, value: unknown): void {
+  const tmp = `${path}.tmp`;
+  writeFileSync(tmp, JSON.stringify(value, null, 2), { mode: 0o600 });
+  try {
+    if (statSync(tmp).mode & 0o077) chmodSync(tmp, 0o600);
+  } catch {
+    // best-effort
+  }
+  renameSync(tmp, path);
+}
+
 export function writeReminder(reminder: StoredReminder): void {
   const p = getReminderFilePath();
   const dir = dirname(p);
   if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
-  writeFileSync(p, JSON.stringify(reminder, null, 2), { mode: 0o600 });
-  try {
-    if (statSync(p).mode & 0o077) chmodSync(p, 0o600);
-  } catch {
-    // best-effort
-  }
+  writeJsonAtomically(p, reminder);
 }
 
 export function writeAuth(auth: StoredAuth): void {
   const p = getAuthFilePath();
   const dir = dirname(p);
   if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
-  writeFileSync(p, JSON.stringify(auth, null, 2), { mode: 0o600 });
-  try {
-    if (statSync(p).mode & 0o077) chmodSync(p, 0o600);
-  } catch {
-    // best-effort
-  }
+  writeJsonAtomically(p, auth);
 }

Also applies to: 139-152

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/auth/auth-store.ts` around lines 87 - 97, The writeReminder function
performs in-place writes which can produce truncated JSON on concurrent writes
or crashes; change it (and any other file-writers in this module) to perform
atomic replace by writing JSON to a temp file in the same directory (use a
randomized suffix), set the temp file permissions to 0o600, fsync the temp file,
rename the temp file to the real path (atomic on POSIX), then fsync the
directory; keep the existing best-effort chmod/exists logic but ensure errors
are handled and cleaned up (unlink temp on error) so readers never see a
partially written file.
lib/auth/auth-store.ts-201-209 (1)

201-209: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't collapse transient upstream failures into "logged out".

Both refresh and /me verification return null for network/upstream errors, so /api/auth/status ends up reporting authenticated: false during transient outages. That turns a temporary auth-service problem into an anonymous state change in the UI.

A separate "unavailable" path here would let callers preserve the last known auth state instead of dropping the session on transport errors.

Also applies to: 223-240

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/auth/auth-store.ts` around lines 201 - 209, The current catch blocks in
the auth-store (which check for AuthApiError and call deleteAuth()) collapse
network/upstream errors into a "logged out" null return; instead, leave null
only for unrecoverable 401 cases and return a distinct "unavailable" signal for
transient failures so callers can preserve the last known session. Concretely:
in the catch blocks that reference AuthApiError and call deleteAuth(), keep the
deleteAuth()+null behavior for err instanceof AuthApiError && err.status ===
401, but for all other errors return or throw a dedicated marker (e.g.,
AuthUnavailable / throw new AuthUnavailableError) rather than null; apply this
change to both catch sites (the refresh/token path and the /me verification
path) and update consumers (e.g., getAuthStatus or the /api/auth/status handler)
to treat that marker as "service unavailable" instead of clearing the session.
lib/auth/api-server-client.ts-99-118 (1)

99-118: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout to the shared upstream fetch helpers.

Both helpers can wait on fetch() indefinitely. In the CLI that can hang failproofai auth forever, and in the dashboard routes it can pin a request until the platform times it out.

Suggested fix
+const FETCH_TIMEOUT_MS = 10_000;
+
 async function postJson<T>(path: string, body: unknown, init?: { accessToken?: string }): Promise<T> {
   const headers: Record<string, string> = { "content-type": "application/json" };
   if (init?.accessToken) headers["authorization"] = `Bearer ${init.accessToken}`;
   const res = await fetch(`${getApiBase()}${path}`, {
     method: "POST",
     headers,
     body: JSON.stringify(body),
+    signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
   });
   if (res.status === 204) return undefined as T;
   if (!res.ok) throw await parseError(res);
   return (await res.json()) as T;
 }
 
 async function getJson<T>(path: string, accessToken: string): Promise<T> {
   const res = await fetch(`${getApiBase()}${path}`, {
     method: "GET",
     headers: { authorization: `Bearer ${accessToken}` },
+    signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
   });
   if (!res.ok) throw await parseError(res);
   return (await res.json()) as T;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/auth/api-server-client.ts` around lines 99 - 118, postJson and getJson
can hang indefinitely because fetch has no timeout; fix both by using an
AbortController: create an AbortController inside postJson and getJson, pass
controller.signal to fetch, start a timeout (configurable or default, e.g., 10s)
that calls controller.abort(), and clear the timeout after fetch completes;
ensure any aborts propagate as errors (so existing parseError handling still
runs) and that you pass the signal in the fetch init for functions postJson and
getJson.
src/audit/archetypes.ts-894-939 (1)

894-939: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add direct unit coverage for the classifier branches.

This introduces three distinct outcome rules plus the 40% secondary cutoff, but the supplied __tests__ changes don't exercise any of them. Please add table-driven cases for zero-signal → precision, broad-spread → goldfish, and secondary fallback vs. promotion so these thresholds stay stable.

As per coding guidelines "Always add unit tests for new behaviour. Place tests in tests/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audit/archetypes.ts` around lines 894 - 939, Add direct unit tests for
classifyAgent to cover its three branching rules: create table-driven tests in
__tests__ that (1) pass an AuditResult with no signals (result.results empty or
hits=0) and assert archetype === "precision" and totalSignal === 0; (2) craft
results that map via SIGNAL_MAP to at least 5 non-zero archetypes with
top3Sum/totalSignal < 0.6 and assert archetype === "goldfish" and secondary
equals the highest-weighted archetype; and (3) exercise the 40% secondary cutoff
by building two cases where the second-highest weight is just above 40% of
primary (expect secondary promoted to sorted[1][0]) and just below 40% (expect
secondary === ARCHETYPES[primary].secondary); reference classifyAgent,
SIGNAL_MAP, ARCHETYPES and AuditResult when constructing those cases.
app/audit/_components/identity-section.tsx-43-47 (1)

43-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't claim all key policies are live for every A-tier result.

The positive branch keys off grade === "A", but A-tier audits can still have missing > 0. That makes the shared LinkedIn copy contradict the actual findings for some users.

📝 Suggested fix
 function buildLinkedInTemplate(score: number, archetypeName: string, grade: Grade, missing: number): string {
-  const verdict = (grade === "S" || grade === "A")
+  const verdict = missing === 0
     ? `${score}/100 — ${grade} tier. every key policy is live. the audit confirmed what good looks like.`
     : `${score}/100 — ${grade} tier. ${missing} prescribed polic${missing === 1 ? "y" : "ies"} uncovered — each one is a real attack surface.`;
   return `We ran a failproofai security audit on our AI agent stack.\n\n${verdict}\n\nArchetype: ${archetypeName.toLowerCase()}. failproofai maps your agent\'s behavior pattern, identifies the exposure, and prescribes the exact policies to close it.\n\nFree. Open-source. 30 seconds to run: ${SITE_URL}`;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/audit/_components/identity-section.tsx` around lines 43 - 47, The
LinkedIn copy in buildLinkedInTemplate incorrectly assumes all A-tier results
have no missing policies; change the conditional that builds `verdict` so that
the positive branch requires both `grade === "S" || grade === "A"` AND `missing
=== 0`, e.g., check `grade === "S" || (grade === "A" && missing === 0)`, so that
when `missing > 0` the template uses the negative branch which mentions
uncovered policies; update only the condition used to select the message (leave
the wording of each branch intact) to ensure the copy matches actual findings.
src/audit/scoring.ts-97-112 (1)

97-112: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cap projected recovery with the same buckets as deriveScore.

projectedScore adds back raw hit weights, but deriveScore only ever subtracts up to 25/15/10 per bucket. Once a bucket is already capped, extra hits keep inflating the projection even though they never lowered the current score, so this can promise a much larger jump than enabling the policies can actually produce.

🔧 Suggested fix
 export function projectedScore(result: AuditResult, currentScore: number): number {
-  let recoverable = 0;
+  let denyRecoverable = 0;
+  let instructRecoverable = 0;
+  let sanitizeRecoverable = 0;
   for (const row of result.results) {
     if (row.source !== "builtin") continue;
     if (row.enabledInConfig) continue;
-    if (row.severity === "deny") recoverable += row.hits * 1.2;
-    else if (row.severity === "instruct" || row.severity === "warn") recoverable += row.hits * 0.7;
-    else recoverable += row.hits * 0.4;
+    if (row.severity === "deny") denyRecoverable += row.hits * 1.2;
+    else if (row.severity === "instruct" || row.severity === "warn") instructRecoverable += row.hits * 0.7;
+    else sanitizeRecoverable += row.hits * 0.4;
   }
-  const proj = Math.min(92, currentScore + Math.round(recoverable));
+  const recoverable =
+    Math.min(denyRecoverable, 25) +
+    Math.min(instructRecoverable, 15) +
+    Math.min(sanitizeRecoverable, 10);
+  const proj = Math.min(92, currentScore + Math.round(recoverable));
   return Math.max(currentScore, proj);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audit/scoring.ts` around lines 97 - 112, projectedScore currently sums
weighted recoverable hits directly, which can exceed the per-severity caps used
by deriveScore; modify projectedScore to aggregate recoverable points by
severity (for builtin && !enabledInConfig rows) and apply the same per-bucket
caps as deriveScore (e.g., deny cap 25, instruct cap 15, warn/other cap 10)
before summing them, then proceed with Math.min(92, currentScore +
Math.round(cappedRecoverable)) and Math.max(currentScore, proj); update the loop
in projectedScore to compute per-severity totals, apply the caps, and then
combine.
src/audit/findings.ts-210-298 (1)

210-298: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add unit tests for detector mapping and card derivation.

This file adds ranking, detector-to-policy remapping, enabled-state logic, relative-time formatting, and fallback copy without any targeted unit coverage. A few focused tests in __tests__/audit/ would catch regressions here quickly.

As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audit/findings.ts` around lines 210 - 298, Add unit tests under
__tests__/audit/ that exercise deriveFindings and buildCard behavior: create
AuditResult fixtures with builtin rows and detector rows to verify sorting by
hits, detector-to-policy remapping via DETECTOR_TO_POLICY, that fix slug uses
mapping.primary when present, that POLICY_META fallbacks (displayTitle/impact)
and FINDING_COPY fallbacks are used for body/cost/desc, that alreadyEnabled
logic respects enabledSet and enabledInConfig, that evidence caps at 4 and adds
a "no example commands captured." comment when empty, and that lastSeen is
formatted via relTimeAgo; assert expected FindingCard fields (num, title
lowercased, install command `failproof policy add ${slug}`, projects, count, and
alsoCoveredBy) for each case.
src/audit/strengths.ts-39-50 (1)

39-50: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Clean-rate is computed from finding hits, not dirty tool calls.

Line 42 subtracts totals.hits from eventsScanned, but totals.hits increments once per policy/detector fire. A single tool call can contribute multiple hits, so this can understate cleanliness or clamp to 0% on mixed audits. Either aggregate a distinct eventsWithHits count upstream or stop labeling this as "clean tool calls".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audit/strengths.ts` around lines 39 - 50, The cleanRate calculation is
wrong because totals.hits counts detector hits (can be many per event) but the
UI labels it as "clean tool calls"; update the code in the block around
variables events, totalHits, detectorsTriggered, cleanRate and the out.push call
so it either uses a distinct eventsWithHits count (if available upstream) to
compute cleanRate as (events - eventsWithHits)/events, or if that upstream count
is not available, stop implying per-tool-call cleanliness: compute a hit-based
rate (totalHits / events or hits-per-event) and change the unit/headline/detail
text in the out.push to reflect "hits" (e.g., "clean hits" or "hit-based clean
rate") instead of "clean tool calls". Ensure updates target the cleanRate
variable and the corresponding unit/headline/detail strings.
src/audit/strengths.ts-37-138 (1)

37-138: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add unit coverage for the new strengths derivation.

This module adds user-facing ranking and fallback logic, but there isn't a matching test suite for cases like the clean-rate headline, zero-hit gates, and the 5-item cap. Please add coverage under __tests__/audit/.

As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audit/strengths.ts` around lines 37 - 138, Add unit tests for
deriveStrengths to cover its new ranking and fallback logic: write tests under
__tests__/audit/ that call deriveStrengths with crafted AuditResult fixtures
exercising (1) the clean-rate headline when eventsScanned > 0 and
detectorsTriggered > 0, (2) the zero-credential gate by ensuring hitsForShort
returns 0 for credentialPolicies, (3) the retry/gitrewrite/wasteful-edit
zero-hit gates (use inputs that drive retryHits, gitHits, wastefulEdits to 0),
(4) average session length branches (avgTurns <15, between 15–29, and >=30), and
(5) the cap-to-5 behavior and the fallback “audit complete” entry when
out.length < 2. Use the deriveStrengths function from strengths.ts and create
minimal AuditResult fixtures (mock totals.hits, transcripts.scanned,
eventsScanned, results array) and assert Strength array contents (metrics,
headlines, units) for each scenario; mock or stub hitsForShort behavior if
needed to target specific policy groups.
src/audit/findings.ts-290-293 (1)

290-293: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the real CLI install command in the fix CTA.

The finding cards currently emit failproof policy add ..., but the rest of the audit flow uses failproofai policies --install .... Copying this from the dashboard will send users to a different command surface than the one exposed elsewhere in this PR.

💡 Proposed fix
-      install: `failproof policy add ${fixSlug}`,
+      install: `failproofai policies --install ${fixSlug}`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audit/findings.ts` around lines 290 - 293, The fix object's install CTA
currently uses the wrong CLI; update the install string in the fix block (where
fix: { slug: fixSlug, desc: fixDesc, install: ... } is defined) to use the
consistent CLI command used elsewhere: `failproofai policies --install
${fixSlug}` so the generated card copies the same command surface as the rest of
the audit flow.
src/audit/dashboard-cache.ts-40-48 (1)

40-48: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Invalidate old or null cache shapes before returning them.

The current guard accepts params: null, result: null, and any historical AuditResult version. After this schema bump, an existing on-disk v1 cache will still be returned even though it lacks projectsScanned, eventsScanned, and enabledBuiltinNames, which can break the dashboard until the user re-runs the audit.

💡 Proposed fix
     const raw = readFileSync(cachePath, "utf-8");
     const entry = JSON.parse(raw) as DashboardCacheEntry;
     if (
       typeof entry?.cachedAt !== "string"
-      || typeof entry?.params !== "object"
-      || typeof entry?.result !== "object"
+      || !entry?.params
+      || typeof entry.params !== "object"
+      || !entry?.result
+      || typeof entry.result !== "object"
+      || entry.result.version !== 2
+      || !Array.isArray(entry.result.projectsScanned)
+      || typeof entry.result.eventsScanned !== "number"
+      || !Array.isArray(entry.result.enabledBuiltinNames)
     ) {
       return null;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audit/dashboard-cache.ts` around lines 40 - 48, The guard in
dashboard-cache.ts currently accepts null or old-version shapes; update the
validation after JSON.parse (the const entry = JSON.parse(raw) as
DashboardCacheEntry line) to return null if entry.params or entry.result are
null or not plain objects, and also verify required v2 AuditResult fields exist
and have correct types (e.g., entry.result.projectsScanned is a number,
entry.result.eventsScanned is a number, and entry.result.enabledBuiltinNames is
an array). Keep the cachedAt string check, and ensure any
missing/incorrectly-typed required fields cause the function to return null so
only up-to-date DashboardCacheEntry shapes are returned.
app/api/audit/run/route.ts-50-78 (1)

50-78: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add route tests for the new run contract.

This endpoint adds important behavior with a few edge paths: malformed-but-valid JSON (null), 409 when a run is already active, and the success path that writes cache. Please cover those under __tests__/ so regressions in the rerun flow get caught early. As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/. "

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/audit/run/route.ts` around lines 50 - 78, Add unit tests under
__tests__/ for the POST route behavior in route.ts: cover the
malformed-but-valid JSON case (body "null") returning 400, the concurrency case
when tryAcquireRun() returns false producing a 409 with { status:
"already-running" }, and the successful path where sanitize(body) is used,
runAudit(opts) resolves and writeDashboardCache(opts, result) is called and the
response is { status: "ok", result }; mock/stub tryAcquireRun, runAudit,
writeDashboardCache, and releaseRun to assert they are invoked appropriately and
that releaseRun() runs in finally.
app/audit/_components/rerun-button.tsx-46-79 (1)

46-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't treat failed reruns as completed reruns.

triggerRun() resolves on POST failures, network failures, and poll timeout, so callers can't distinguish success from failure. In ReturnSection that means a stale page reload still happens after a failed rerun. Return an explicit status or throw so the caller only runs its completion path when a scan actually finished.

Proposed fix
-export async function triggerRun(scanParams: ScanParams): Promise<void> {
+export async function triggerRun(scanParams: ScanParams): Promise<boolean> {
   try {
     const res = await fetch("/api/audit/run", {
       method: "POST",
       headers: { "content-type": "application/json" },
       body: JSON.stringify(paramsToBody(scanParams)),
     });
     if (!res.ok && res.status !== 409) {
       const text = await res.text().catch(() => "");
       console.error("audit run failed:", res.status, text);
-      return;
+      return false;
     }
   } catch (err) {
     console.error("audit run request failed:", err);
-    return;
+    return false;
   }

   const startedAt = Date.now();
   while (Date.now() - startedAt < MAX_POLL_MS) {
     await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
     try {
       const sres = await fetch("/api/audit/status", { cache: "no-store" });
       if (!sres.ok) continue;
       const s = await sres.json() as { running: boolean };
-      if (!s.running) return;
+      if (!s.running) return true;
     } catch {
       // Transient — keep polling.
     }
   }
+
+  return false;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/audit/_components/rerun-button.tsx` around lines 46 - 79, triggerRun
currently resolves on POST failures, network errors, and poll timeouts so
callers (e.g., ReturnSection) can't tell success from failure; change triggerRun
to throw on any POST non-OK (except allowed 409) and on network/fetch errors and
to throw if polling exceeds MAX_POLL_MS without seeing running flip to false,
and only resolve (return) when the poll observes the scan finished; include
response text or error details in thrown Error messages (reference triggerRun,
paramsToBody, /api/audit/run, /api/audit/status, MAX_POLL_MS, POLL_INTERVAL_MS)
so callers can run completion logic only on true success.
app/api/audit/run/route.ts-51-59 (1)

51-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-object JSON bodies before calling sanitize().

JSON.parse("null") succeeds, so body becomes null here and sanitize(body) throws on Line 59. That turns a bad request into a 500. Validate that the parsed payload is a plain object and return 400 otherwise.

Proposed fix
 export async function POST(request: NextRequest): Promise<NextResponse> {
   let body: RunBody = {};
   try {
     const raw = await request.text();
-    if (raw) body = JSON.parse(raw) as RunBody;
+    if (raw) {
+      const parsed: unknown = JSON.parse(raw);
+      if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+        return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
+      }
+      body = parsed as RunBody;
+    }
   } catch {
     return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/audit/run/route.ts` around lines 51 - 59, The parsed JSON may be
non-object (e.g., null or an array) which causes sanitize(body) to throw; after
parsing the request text (the result of request.text() and JSON.parse), validate
that the parsed value is a plain non-null object (typeof parsed === "object" &&
parsed !== null && !Array.isArray(parsed)) before assigning to body and calling
sanitize(body); if the check fails, return NextResponse.json({ error: "Invalid
JSON body" }, { status: 400 }) so sanitize() only receives a proper object
(refer to the RunBody variable, the local body/parsing logic, and the sanitize()
call).
app/api/audit/_state.ts-21-39 (1)

21-39: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Move the run lock out of module memory.

app/api/audit/_state.ts keeps the guard in module-level in-memory variables, so it only coordinates within a single Node.js process/worker. If your deployment runs multiple replicas/processes, /api/audit/run can acquire the lock in one worker while /api/audit/status polls another (or another run is accepted), making running/concurrency incorrect. Use a shared/distributed lock+status store (e.g., Redis/DB with atomic acquire and TTL) instead of module memory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/audit/_state.ts` around lines 21 - 39, The module-level lock
represented by state and the functions tryAcquireRun, releaseRun, and
getRunState must be replaced with a distributed lock/status stored in a shared
system (e.g., Redis or DB): change tryAcquireRun to perform an atomic acquire
(e.g., Redis SET key value NX EX ttl) and store a unique owner token and
startedAt in the shared store returning true only on success, change releaseRun
to release only if the owner token matches (to avoid deleting another process's
lock), and change getRunState to read running and startedAt from the shared
store; ensure TTL is used to avoid stuck locks and persist startedAt alongside
the lock.
app/audit/_components/run-progress.tsx-29-37 (1)

29-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the progress UI below “complete” until the run actually finishes.

This animation hits 4/4 and 100% after 16 seconds, but the copy says runs can take up to ~30 seconds. On slower audits the screen looks done while the request is still pending, which reads like a hang.

💡 One way to avoid the false-complete state
 export function RunProgress() {
   const [stage, setStage] = useState(0);
   const [tick, setTick] = useState(0);
+  const atLastStage = stage === STAGES.length - 1;
+  const progressPct = atLastStage ? 90 : ((stage + 1) / STAGES.length) * 100;

   useEffect(() => {
@@
         <div className="running-bar-label">
           <span>progress</span>
-          <span style={{ color: "var(--dim)" }}>{stage + 1}/{STAGES.length}</span>
+          <span style={{ color: "var(--dim)" }}>
+            {atLastStage ? "finishing up" : `${stage + 1}/${STAGES.length}`}
+          </span>
         </div>
         <div className="running-bar-track">
           <div
             className="running-bar-fill"
-            style={{ width: `${((stage + 1) / STAGES.length) * 100}%` }}
+            style={{ width: `${progressPct}%` }}
           />
         </div>

Also applies to: 88-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/audit/_components/run-progress.tsx` around lines 29 - 37, The progress UI
currently advances through all STAGES based purely on STAGE_DURATION_MS inside
the useEffect (timers updating setStage and setTick), causing it to reach the
final "complete" stage before the audit actually finishes; modify the effect to
cap stage progression at STAGES.length - 2 (or otherwise prevent reaching the
final index) while the run is still pending, and only allow setStage to advance
to STAGES.length - 1 when a real completion flag (e.g., an isFinished/isComplete
prop or state tied to the audit request) is true; keep the existing tick timer
behavior but ensure the stageTimer callback checks that completion flag before
calling setStage((s) => Math.min(s + 1, STAGES.length - 1)) so the UI only shows
100% when the run has truly finished.
app/audit/_components/policies-section.tsx-63-110 (1)

63-110: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add unit coverage for this policy derivation.

This mapping/aggregation logic now defines the report output, but the supplied tests only cover cache/replay behavior. Please add cases for detector mapping, enabled-policy exclusion, and multi-source aggregation under __tests__/audit/. As per coding guidelines, "Always add unit tests for new behaviour. Place tests in tests/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/audit/_components/policies-section.tsx` around lines 63 - 110, Add unit
tests for buildPolicyCards in a new test under __tests__/audit/ that exercise
detector-to-primary-policy mapping, exclusion when a policy is already enabled,
and aggregation from multiple sources: construct AuditResult objects with rows
where source === "audit-detector" that map via DETECTOR_TO_PRIMARY_POLICY (use
shortName(row.name) keys), rows with source === "builtin" and both
enabledInConfig true/false to verify enabled ones are skipped, and multiple rows
that target the same policy to confirm hits/projects/sources aggregate into one
PolicyCard; assert the returned PolicyCard array (from buildPolicyCards)
contains expected name, hits, projects, desc (from POLICY_DESC fallback), and
that the catches string includes the via list when applicable. Ensure tests
import buildPolicyCards, DETECTOR_TO_PRIMARY_POLICY, shortName and use
enabledBuiltinNames in AuditResult to cover exclusion behavior.
app/audit/_components/policies-section.tsx-152-178 (1)

152-178: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the actual CLI binary in the copied install command.

This builds failproof policy add …, but the rest of this PR surfaces the CLI as failproofai. Copying the current string will hand users a command that doesn't match the shipped binary.

🔧 Proposed fix
-  const install = `failproof policy add ${policy.name}`;
+  const install = `failproofai policy add ${policy.name}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/audit/_components/policies-section.tsx` around lines 152 - 178, The
install string in PolicyTile is using the wrong CLI binary name; update the
install template in the PolicyTile component (where install is defined) from
"failproof policy add ${policy.name}" to "failproofai policy add ${policy.name}"
so the copied command matches the shipped binary, ensuring handleCopy still
writes the updated install variable to the clipboard.
assets/audit/poster.jsx-93-100 (1)

93-100: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the current poster params when linking back to the audit report.

Both back links drop the active a/s/g/r/c/p query string, so a poster opened directly or shared externally can't reconstruct the same audit view and falls back to defaults instead.

🩹 Suggested direction
 function Poster() {
+  const auditHref = "Audit Report.html" + window.location.search;
   const key = getParam("a", "optimist");
@@
-    window.location.href = "Audit Report.html";
+    window.location.href = auditHref;
@@
-          <a className="poster-back" href="Audit Report.html" onClick={handleBack}>
+          <a className="poster-back" href={auditHref} onClick={handleBack}>
@@
-          <a href="Audit Report.html" style={{ color: "var(--ink-2)" }}>view full audit →</a>
+          <a href={auditHref} style={{ color: "var(--ink-2)" }}>view full audit →</a>

Also applies to: 107-107, 239-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/audit/poster.jsx` around lines 93 - 100, The back-link handler
handleBack currently navigates to "Audit Report.html" without preserving poster
query params (a/s/g/r/c/p); update handleBack (and the other back-link handlers
referenced) to capture the current window.location.search (or filter and rebuild
a search string keeping only keys a, s, g, r, c, p) and append it to "Audit
Report.html" (e.g., "Audit Report.html" + "?" + preservedSearch) before setting
window.location.href, and ensure e.preventDefault() remains when redirecting.
assets/audit/archetypes.jsx-246-266 (1)

246-266: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the Sigil fallback consistent.

Line 247 falls back to SIGILS.optimist, but Line 265 still reads ARCHETYPES[archetypeKey].index directly. Any unknown archetypeKey will still crash render even though the grid fallback succeeded.

🩹 Proposed fix
 function Sigil({ archetypeKey }) {
-  const grid = SIGILS[archetypeKey] || SIGILS.optimist;
+  const archetype = ARCHETYPES[archetypeKey] || ARCHETYPES.optimist;
+  const grid = SIGILS[archetype.key] || SIGILS.optimist;
   const cells = [];
   for (let y = 0; y < 8; y++) {
     const row = grid[y] || "........";
@@
       <div className="sigil">{cells}</div>
       <div className="sigil-label">
-        <span className="ix">№{ARCHETYPES[archetypeKey].index}</span>
+        <span className="ix">№{archetype.index}</span>
         sigil
       </div>
     </div>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/audit/archetypes.jsx` around lines 246 - 266, The Sigil component uses
SIGILS[archetypeKey] with a fallback but still accesses
ARCHETYPES[archetypeKey].index directly which can crash for unknown keys; update
Sigil to compute a single safe fallback (e.g., const archetype =
ARCHETYPES[archetypeKey] || ARCHETYPES.optimist and const grid =
SIGILS[archetypeKey] || SIGILS.optimist) and then use archetype.index and any
archetype properties for the label instead of indexing ARCHETYPES again with
archetypeKey so both grid and metadata consistently use the fallback.
assets/audit/audit.jsx-18-26 (1)

18-26: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize numeric query params before storing defaults.

Lines 20-22 accept parseInt(...) results verbatim, so malformed ?s=, ?r=, or ?c= values become NaN and then flow into score math, toLocaleString(), and rank/percentile copy across the page. This is user-reachable from the URL.

🩹 Suggested direction
+function getIntParam(name, fallback, { min, max } = {}) {
+  const raw = Number.parseInt(getParam(name, String(fallback)), 10);
+  if (!Number.isFinite(raw)) return fallback;
+  const lower = min ?? raw;
+  const upper = max ?? raw;
+  return Math.min(upper, Math.max(lower, raw));
+}
+
 const REPORT_DEFAULTS = /*EDITMODE-BEGIN*/{
   "archetype": getParam("a", "optimist"),
-  "score": parseInt(getParam("s", "58"), 10),
-  "rank": parseInt(getParam("r", "1847"), 10),
-  "cohort": parseInt(getParam("c", "2316"), 10),
+  "score": getIntParam("s", 58, { min: 0, max: 100 }),
+  "rank": getIntParam("r", 1847, { min: 1 }),
+  "cohort": getIntParam("c", 2316, { min: 1 }),
   "tweetVariant": "show-off",
   "showSecondary": true,
   "project": getParam("p", "blrnow / api-coder")
 }/*EDITMODE-END*/;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/audit/audit.jsx` around lines 18 - 26, REPORT_DEFAULTS currently
assigns score, rank, and cohort using parseInt(getParam(...)) directly which can
yield NaN from malformed query params; update the initialization in
REPORT_DEFAULTS so that for each numeric field (score, rank, cohort) you parse
the param (using parseInt(..., 10) as already done), then validate the result
(Number.isFinite or !Number.isNaN) and if invalid fall back to the original
hardcoded defaults (58, 1847, 2316 respectively); reference the REPORT_DEFAULTS
object and getParam usages so the fallback logic is applied inline where those
values are set.
assets/audit/tweaks-panel.jsx-192-200 (1)

192-200: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the postMessage sender before accepting edit-mode commands.

assets/audit/tweaks-panel.jsx toggles the panel based solely on e.data.type (__activate_edit_mode / __deactivate_edit_mode) with no e.source/e.origin checks, so any frame can drive the host edit-mode UI. Gate on the expected sender at minimum (e.source === window.parent), and validate e.origin if the embed origin is known.

🩹 Proposed fix
   React.useEffect(() => {
     const onMsg = (e) => {
+      if (e.source !== window.parent) return;
       const t = e?.data?.type;
       if (t === '__activate_edit_mode') setOpen(true);
       else if (t === '__deactivate_edit_mode') setOpen(false);
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/audit/tweaks-panel.jsx` around lines 192 - 200, The message handler
inside the React.useEffect (onMsg) currently toggles setOpen based only on
e.data.type; update onMsg to first verify the sender by checking e.source ===
window.parent and, if you know the embed origin, also validate e.origin against
that expected origin before acting on __activate_edit_mode /
__deactivate_edit_mode; keep the window.parent.postMessage call but if an
expected origin is available use it instead of '*' and ensure the cleanup still
removes the same onMsg listener.
assets/audit/poster.jsx-85-90 (1)

85-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix async clipboard copy success state and preserve poster query params when navigating back

  • assets/audit/poster.jsx (lines 85-91): navigator.clipboard.writeText(...) is Promise-based; the current try/catch won’t catch permission/insecure-context failures, yet the UI still flips to [ link copied ]. Make the handler async and only set setCopied(true) after a successful await, and handle failures.
🩹 Proposed fix
-  const handleCopyLink = () => {
+  const handleCopyLink = async () => {
     try {
-      navigator.clipboard.writeText(window.location.href);
+      await navigator.clipboard.writeText(window.location.href);
       setCopied(true);
       setTimeout(() => setCopied(false), 1600);
-    } catch (e) {}
+    } catch (e) {
+      setCopied(false);
+    }
   };
- `assets/audit/poster.jsx` (lines 93-100, link at 107, footer link at 239): navigating to `"Audit Report.html"` drops `window.location.search`, so the shared poster state (`a/s/g/r/c/p`) isn’t preserved. Append the current `window.location.search` to the destination (and reuse the same navigation logic for the footer link too).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/audit/poster.jsx` around lines 85 - 90, The handleCopyLink handler
currently calls navigator.clipboard.writeText(...) without awaiting the Promise
and flips UI state prematurely; make handleCopyLink async, await
navigator.clipboard.writeText(window.location.href) inside a try/catch, call
setCopied(true) only after a successful await and handle/log failures in the
catch (and still avoid unhandled rejections), and keep the setTimeout to clear
the state; additionally, when building navigation targets to "Audit Report.html"
(the link at the in-file link and the footer link), preserve poster query params
by appending window.location.search to the destination URL (reuse the same
query-appending logic for both the inline link and the footer link so the shared
poster state a/s/g/r/c/p is retained).
🧹 Nitpick comments (1)
app/audit/_components/policies-section.tsx (1)

28-39: ⚡ Quick win

Share the detector→policy map instead of mirroring it.

Keeping a second hard-coded copy here can drift from src/audit/findings.ts, which would make the Findings and Policies sections disagree for the same detector. Export the mapping from one place or move it into shared audit metadata.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/audit/_components/policies-section.tsx` around lines 28 - 39, The local
DETECTOR_TO_PRIMARY_POLICY constant in policies-section.tsx is a duplicate of
the mapping in src/audit/findings.ts (DETECTOR_TO_POLICY); remove the hard-coded
copy and instead re-export or share the single source of truth: either export
DETECTOR_TO_PRIMARY_POLICY (or alias DETECTOR_TO_POLICY) from findings.ts or
move the mapping into a new shared audit metadata module and import it into
policies-section.tsx and findings.ts; update the import in
app/audit/_components/policies-section.tsx to use the shared export and delete
the local const to avoid drift between Findings and Policies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64607466-755d-46c1-b890-734e8f797f75

📥 Commits

Reviewing files that changed from the base of the PR and between 1a37c48 and 356bd17.

⛔ Files ignored due to path filters (12)
  • app/icon.png is excluded by !**/*.png
  • assets/audit/assets/fonts/architype-stedelijk.ttf is excluded by !**/*.ttf
  • assets/audit/assets/fonts/architype-stedelijk.woff2 is excluded by !**/*.woff2
  • assets/audit/screenshots/poster-optimist.png is excluded by !**/*.png
  • assets/audit/screenshots/poster-scrolled.png is excluded by !**/*.png
  • assets/logos/company/icon.svg is excluded by !**/*.svg
  • assets/logos/company/logo.svg is excluded by !**/*.svg
  • bun.lock is excluded by !**/*.lock
  • public/audit/fonts/architype-stedelijk.ttf is excluded by !**/*.ttf
  • public/audit/fonts/architype-stedelijk.woff2 is excluded by !**/*.woff2
  • public/icon.svg is excluded by !**/*.svg
  • public/logo.svg is excluded by !**/*.svg
📒 Files selected for processing (62)
  • CHANGELOG.md
  • __tests__/audit/dashboard-cache.test.ts
  • __tests__/audit/replay.test.ts
  • app/actions/get-audit-result.ts
  • app/api/audit/_state.ts
  • app/api/audit/run/route.ts
  • app/api/audit/status/route.ts
  • app/api/auth/login-request/route.ts
  • app/api/auth/login-verify/route.ts
  • app/api/auth/logout/route.ts
  • app/api/auth/reminder/route.ts
  • app/api/auth/status/route.ts
  • app/audit/_components/audit-dashboard.tsx
  • app/audit/_components/auth-dialog.tsx
  • app/audit/_components/empty-state.tsx
  • app/audit/_components/findings-section.tsx
  • app/audit/_components/identity-section.tsx
  • app/audit/_components/policies-section.tsx
  • app/audit/_components/report-footer.tsx
  • app/audit/_components/rerun-button.tsx
  • app/audit/_components/return-section.tsx
  • app/audit/_components/run-progress.tsx
  • app/audit/_components/score-section.tsx
  • app/audit/_components/show-off-cta.tsx
  • app/audit/_components/sigil.tsx
  • app/audit/_components/strengths-section.tsx
  • app/audit/audit-styles.css
  • app/audit/loading.tsx
  • app/audit/page.tsx
  • app/globals.css
  • app/layout.tsx
  • app/policies/hooks-client.tsx
  • app/projects/loading.tsx
  • app/projects/page.tsx
  • assets/audit/Audit Report.html
  • assets/audit/Show Off Your Agent.html
  • assets/audit/archetypes.jsx
  • assets/audit/audit.jsx
  • assets/audit/poster-styles.css
  • assets/audit/poster.jsx
  • assets/audit/styles.css
  • assets/audit/tweaks-panel.jsx
  • bin/failproofai.mjs
  • components/navbar.tsx
  • components/reach-developers.tsx
  • docs/cli/auth.mdx
  • docs/cli/environment-variables.mdx
  • docs/dashboard.mdx
  • eslint.config.mjs
  • lib/auth/api-server-client.ts
  • lib/auth/auth-store.ts
  • package.json
  • src/audit/archetypes.ts
  • src/audit/dashboard-cache.ts
  • src/audit/findings.ts
  • src/audit/index.ts
  • src/audit/replay.ts
  • src/audit/scoring.ts
  • src/audit/strengths.ts
  • src/audit/types.ts
  • src/auth/cli.ts
  • src/hooks/policy-registry.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/auth/cli.ts`:
- Around line 124-128: The ANSI color constants DIM, RESET, PINK, GREEN, and RED
are missing the ESC prefix so they print raw text; update each constant (DIM,
RESET, PINK, GREEN, RED in the cli.ts snippet) to include the ESC character
(e.g., "\x1b" or "\u001b") before the bracket so the sequences become "\x1b[2m",
"\x1b[0m", "\x1b[38;5;204m", etc., ensuring proper terminal formatting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a46601e-7489-4bf9-b4ef-04bb25463922

📥 Commits

Reviewing files that changed from the base of the PR and between 356bd17 and 1884dda.

📒 Files selected for processing (4)
  • app/api/auth/status/route.ts
  • app/audit/_components/return-section.tsx
  • bin/failproofai.mjs
  • src/auth/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/audit/_components/return-section.tsx

Comment thread src/auth/cli.ts
Comment on lines +124 to +128
const DIM = "[2m";
const RESET = "[0m";
const PINK = "[38;5;204m";
const GREEN = "[38;5;120m";
const RED = "[38;5;197m";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

ANSI escape sequences are malformed — missing escape character.

The color constants are missing the \x1b (ESC) prefix. As written, they will print literal text like [2m instead of applying terminal formatting.

🐛 Proposed fix
-const DIM = "[2m";
-const RESET = "[0m";
-const PINK = "[38;5;204m";
-const GREEN = "[38;5;120m";
-const RED = "[38;5;197m";
+const DIM = "\x1b[2m";
+const RESET = "\x1b[0m";
+const PINK = "\x1b[38;5;204m";
+const GREEN = "\x1b[38;5;120m";
+const RED = "\x1b[38;5;197m";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const DIM = "[2m";
const RESET = "[0m";
const PINK = "[38;5;204m";
const GREEN = "[38;5;120m";
const RED = "[38;5;197m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";
const PINK = "\x1b[38;5;204m";
const GREEN = "\x1b[38;5;120m";
const RED = "\x1b[38;5;197m";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/auth/cli.ts` around lines 124 - 128, The ANSI color constants DIM, RESET,
PINK, GREEN, and RED are missing the ESC prefix so they print raw text; update
each constant (DIM, RESET, PINK, GREEN, RED in the cli.ts snippet) to include
the ESC character (e.g., "\x1b" or "\u001b") before the bracket so the sequences
become "\x1b[2m", "\x1b[0m", "\x1b[38;5;204m", etc., ensuring proper terminal
formatting.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

An unexpected error occurred while resolving merge conflicts:

Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant