feat(audit): persistent return-loop with auth, reminders, inline re-audit#397
feat(audit): persistent return-loop with auth, reminders, inline re-audit#397SiddarthAA wants to merge 13 commits into
Conversation
… 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>
📝 WalkthroughWalkthroughAdds 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. ChangesEmail OTP Authentication & Audit Dashboard
Estimated code review effort: Possibly Related PRs
✨ Finishing Touches⚔️ Resolve merge conflicts❌ Error resolving conflicts.
|
There was a problem hiding this comment.
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 winKeep the parent summary state in sync after configure-tab changes.
This effect seeds
policyCountsandinstalledCliLabelsonce, butPoliciesTabcan 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 inPoliciesTab, 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 winScope reminder deletion to the active session.
DELETEunconditionally wipesnext-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 return401/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 winDon't block
--loginon a staleauth.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 runfailproofai auth --logoutbefore they can log back in.Use
whoAmI()orgetValidAccessToken()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 liftAdd 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 winReject 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 a400, 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 liftMake 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 liftDon't collapse transient upstream failures into "logged out".
Both refresh and
/meverification returnnullfor network/upstream errors, so/api/auth/statusends up reportingauthenticated: falseduring 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 winAdd a timeout to the shared upstream fetch helpers.
Both helpers can wait on
fetch()indefinitely. In the CLI that can hangfailproofai authforever, 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 winAdd 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 winDon'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 havemissing > 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 winCap projected recovery with the same buckets as
deriveScore.
projectedScoreadds back raw hit weights, butderiveScoreonly 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 winAdd 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 liftClean-rate is computed from finding hits, not dirty tool calls.
Line 42 subtracts
totals.hitsfromeventsScanned, buttotals.hitsincrements once per policy/detector fire. A single tool call can contribute multiple hits, so this can understate cleanliness or clamp to0%on mixed audits. Either aggregate a distincteventsWithHitscount 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 winAdd 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 winUse the real CLI install command in the fix CTA.
The finding cards currently emit
failproof policy add ..., but the rest of the audit flow usesfailproofai 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 winInvalidate old or null cache shapes before returning them.
The current guard accepts
params: null,result: null, and any historicalAuditResultversion. After this schema bump, an existing on-disk v1 cache will still be returned even though it lacksprojectsScanned,eventsScanned, andenabledBuiltinNames, 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 winAdd 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 winDon'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. InReturnSectionthat 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 winReject non-object JSON bodies before calling
sanitize().
JSON.parse("null")succeeds, sobodybecomesnullhere andsanitize(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 liftMove the run lock out of module memory.
app/api/audit/_state.tskeeps 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/runcan acquire the lock in one worker while/api/audit/statuspolls another (or another run is accepted), makingrunning/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 winKeep the progress UI below “complete” until the run actually finishes.
This animation hits
4/4and 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 winAdd 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 winUse the actual CLI binary in the copied install command.
This builds
failproof policy add …, but the rest of this PR surfaces the CLI asfailproofai. 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 winPreserve the current poster params when linking back to the audit report.
Both back links drop the active
a/s/g/r/c/pquery 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 winKeep the
Sigilfallback consistent.Line 247 falls back to
SIGILS.optimist, but Line 265 still readsARCHETYPES[archetypeKey].indexdirectly. Any unknownarchetypeKeywill 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 winSanitize numeric query params before storing defaults.
Lines 20-22 accept
parseInt(...)results verbatim, so malformed?s=,?r=, or?c=values becomeNaNand 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 winValidate the
postMessagesender before accepting edit-mode commands.
assets/audit/tweaks-panel.jsxtoggles the panel based solely one.data.type(__activate_edit_mode/__deactivate_edit_mode) with noe.source/e.originchecks, so any frame can drive the host edit-mode UI. Gate on the expected sender at minimum (e.source === window.parent), and validatee.originif 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 winFix 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 currenttry/catchwon’t catch permission/insecure-context failures, yet the UI still flips to[ link copied ]. Make the handlerasyncand only setsetCopied(true)after a successfulawait, and handle failures.- `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).🩹 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); + } };🤖 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 winShare 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
⛔ Files ignored due to path filters (12)
app/icon.pngis excluded by!**/*.pngassets/audit/assets/fonts/architype-stedelijk.ttfis excluded by!**/*.ttfassets/audit/assets/fonts/architype-stedelijk.woff2is excluded by!**/*.woff2assets/audit/screenshots/poster-optimist.pngis excluded by!**/*.pngassets/audit/screenshots/poster-scrolled.pngis excluded by!**/*.pngassets/logos/company/icon.svgis excluded by!**/*.svgassets/logos/company/logo.svgis excluded by!**/*.svgbun.lockis excluded by!**/*.lockpublic/audit/fonts/architype-stedelijk.ttfis excluded by!**/*.ttfpublic/audit/fonts/architype-stedelijk.woff2is excluded by!**/*.woff2public/icon.svgis excluded by!**/*.svgpublic/logo.svgis excluded by!**/*.svg
📒 Files selected for processing (62)
CHANGELOG.md__tests__/audit/dashboard-cache.test.ts__tests__/audit/replay.test.tsapp/actions/get-audit-result.tsapp/api/audit/_state.tsapp/api/audit/run/route.tsapp/api/audit/status/route.tsapp/api/auth/login-request/route.tsapp/api/auth/login-verify/route.tsapp/api/auth/logout/route.tsapp/api/auth/reminder/route.tsapp/api/auth/status/route.tsapp/audit/_components/audit-dashboard.tsxapp/audit/_components/auth-dialog.tsxapp/audit/_components/empty-state.tsxapp/audit/_components/findings-section.tsxapp/audit/_components/identity-section.tsxapp/audit/_components/policies-section.tsxapp/audit/_components/report-footer.tsxapp/audit/_components/rerun-button.tsxapp/audit/_components/return-section.tsxapp/audit/_components/run-progress.tsxapp/audit/_components/score-section.tsxapp/audit/_components/show-off-cta.tsxapp/audit/_components/sigil.tsxapp/audit/_components/strengths-section.tsxapp/audit/audit-styles.cssapp/audit/loading.tsxapp/audit/page.tsxapp/globals.cssapp/layout.tsxapp/policies/hooks-client.tsxapp/projects/loading.tsxapp/projects/page.tsxassets/audit/Audit Report.htmlassets/audit/Show Off Your Agent.htmlassets/audit/archetypes.jsxassets/audit/audit.jsxassets/audit/poster-styles.cssassets/audit/poster.jsxassets/audit/styles.cssassets/audit/tweaks-panel.jsxbin/failproofai.mjscomponents/navbar.tsxcomponents/reach-developers.tsxdocs/cli/auth.mdxdocs/cli/environment-variables.mdxdocs/dashboard.mdxeslint.config.mjslib/auth/api-server-client.tslib/auth/auth-store.tspackage.jsonsrc/audit/archetypes.tssrc/audit/dashboard-cache.tssrc/audit/findings.tssrc/audit/index.tssrc/audit/replay.tssrc/audit/scoring.tssrc/audit/strengths.tssrc/audit/types.tssrc/auth/cli.tssrc/hooks/policy-registry.ts
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
app/api/auth/status/route.tsapp/audit/_components/return-section.tsxbin/failproofai.mjssrc/auth/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/audit/_components/return-section.tsx
| const DIM = "[2m"; | ||
| const RESET = "[0m"; | ||
| const PINK = "[38;5;204m"; | ||
| const GREEN = "[38;5;120m"; | ||
| const RED = "[38;5;197m"; |
There was a problem hiding this comment.
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.
| 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.
|
❌ An unexpected error occurred while resolving merge conflicts: Not Found - https://docs.github.com/rest/git/refs#get-a-reference |
Summary
Build out the end-to-end "come back better" return-audit loop on the
/auditresults page so a freshly-audited user has a single, persistentplace 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/*.jsonfiles 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/*)~/.failproofai/auth.jsonthat theNext.js routes share with the CLI. Sessions are validated server-side
on every probe; no client-side trust.
GET /api/auth/status— returns{authenticated, user, reminder}in a single round-trip; reminder is hydrated from
~/.failproofai/next-audit.jsonwhen 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) — writesauth.jsonafter the verification step.
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/statusprobe onmount:
unknown(probe in flight)anon[ set a reminder ]opensAuthDialog;[ re-audit now ]+[ install policies ]remain availableauthed[ set a reminder ]Key behaviors:
[ set a reminder ],the dialog auto-persists the 7-day reminder on successful auth — no
second click required.
next-audit.jsonon every mount; refreshing the page does not resetthe loop.
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)triggerRun({cli, since})so both the empty-state CTA andthe in-flow
[ re-audit now ]button share one code path.POST /api/audit/runwith the samesince: "30d"default the dashboard uses, then
window.location.reload()s tore-hydrate the cached result and dashboard with the new scan.
[ scanning… ]) and is no-op while a runis in flight.
Auth dialog (
app/audit/_components/auth-dialog.tsx)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).
{id, email}user shape upstream.Styling (
app/audit/audit-styles.css).return-statuspanel +.rs-row/.rs-dot-pink/.rs-dot-green/.rs-email/.rs-strongprimitives matching theexisting terminal aesthetic (pink primary line, green confirmation
line, subtle grid background tinted with
--accent-pink)..auth-status-pillfor non-section contexts.reminder line for legibility at desktop widths.
Docs
~/.failproofai/next-audit.jsonand the/api/auth/remindercontract alongside the existing~/.failproofai/auth.jsonnotes so both the CLI and the web stayhonest 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
[ set a reminder ] [ re-audit now ] [ install policies ]; no email pill.[ set a reminder ]→ AuthDialog opens; verify → panel flips to status layout with "in 7 days" + signed-in line, single click.[ set a reminder ]; clicking persists without re-opening the dialog.~/.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:rungreen;bun run lint+bunx tsc --noEmitclean.failproofai logout) flips the web UI back to the anonymous CTA on next reload.authed-no-reminder, never user A's reminder.Summary by CodeRabbit
New Features
UI/UX
Documentation