Review/latest updates#1
Conversation
…; fix calibration drift
- Adjacent agents: replace gradient icon-box cards with clean editorial rows (cream icon square, inline text, thin separators — no "AI template" look) - CTA section: forest-green TS card + purple Lua card with brand labels and specific copy; add "What's your next move?" section header - Brief card: fix bc-lbl contrast (#9AA39B → var(--ink-soft)), use var(--paper) bg for bc-box so content is distinct from wrapper - callScoreApi: widen tool-result parsing to cover .tool_results, .output, .name variants; add step[0] dump to console for diagnosing inconsistency - Fix restart() crash when #overlay element not present (null guard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
File contained approved bash commands with API key in plain text. Added to .gitignore so it never gets committed again. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scan step.output and step.result directly (not just step.toolResults) - Shared extractScore() helper validates shape in one place - Match on any step that contains a valid scoring object, not just by tool name - Extra console logs: all tool result count + first tool result dump Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tions Invocation contract now explicit — every message is a programmatic API call. Ada must call the appropriate tool without asking for more info or responding in text. Eliminates the 'clarifying question' failure mode seen in smoke test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- netlify/functions/score-jd.js: calls Lua developer AI endpoint directly with the scoring system prompt + forced tool_choice. One LLM call instead of two (Ada routing + score_jd). Fits comfortably within 26s timeout. - netlify.toml: set functions timeout = 26 - index.html: callScoreApi() uses score-jd endpoint for text JDs; URL-pasted JDs still go through lua-chat (needs scraping path via Ada) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Embeds LUA_AGENT_ID, LUA_API_KEY, SCORING_SYSTEM, and SCORE_ROLE_TOOL directly in frontend JS. callScoreApi() calls Lua developer AI endpoint from the browser for text JDs, bypassing Netlify functions entirely to avoid the 504 timeout caused by the 2-LLM-call chain. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New fetch-jd Netlify function fetches job posting URL server-side (CORS proxy, no AI, fast). Browser then calls Lua AI directly with the extracted text — same single-LLM-call path as text JDs. Eliminates the Ada→scrape_jd→score_jd 3-call chain that caused 504s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root causes fixed: 1. /developer/ai/generate silently drops tools/tool_choice — tool_use never appears in response, score always fails 2. Routing through Netlify proxy causes 504 (Ada skill chain ~20-40s exceeds Netlify's 26s limit) Solution: call Ada's chat/generate endpoint directly from the browser. No Netlify in the scoring path — no timeout. Ada invokes score_jd skill server-side with proper AI.generate + tool_choice. For URL JDs: fetch-jd Netlify function handles CORS (HTML fetch only, no AI, fast) then browser calls Ada with the extracted text. Removes SCORING_SYSTEM and SCORE_ROLE_TOOL from frontend — Ada owns the scoring rubric through her deployed skill. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…DPOINT LUA_CHAT_ENDPOINT was removed in previous commit but still referenced in three places (capture_lead, submit_cta, enrichment chat), causing silent JS errors. Replace all three with ADA_CHAT_URL so all Ada calls use the same direct browser→chat API path — no Netlify env vars needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
score-role.skill.ts:
- Remove tools/tool_choice from AI.generate — Lua platform drops these,
causing tool_choice-without-tools Anthropic 400 error (generation_failed)
- Switch to JSON text output: system prompt instructs Claude to return
a raw JSON object matching the scoring schema
- Add tryParseJson helper + update extractScoringResult to handle both
tool_use (future-proof) and text JSON responses
index.html:
- Replace static ADA_CHAT_URL with adaSessionUrl(prefix) function
- Each evaluation gets a unique channel=eval-{uuid} — eliminates
session contamination from prior failed calls (was 53k tokens/call)
- Enrichment chat uses state.enrichUrl (consistent across turns)
- capture_lead and submit_cta get fresh sessions (lead-/cta- prefix)
- Add Authorization header to all Ada calls (was missing on some paths)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… broken Root cause confirmed: skill's AI.generate() fails with generation_failed on every call (observed in 30 consecutive logs 09:36–11:06). Even after removing tools/tool_choice in v1.0.18, still fails. Developer endpoint tested directly — works perfectly for text + JSON output. Scoring path: Browser → score-jd.js Netlify → developer AI endpoint (JSON text output, no tools) → parse JSON → result. API key stays server-side. 15–20s per call fits within 26s timeout. Ada chat API still used for capture_lead, submit_cta, enrichment chat (those are single LLM steps, no nested AI.generate(), work fine). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /api directory with Vercel function signatures (ESM, req/res):
* score-jd.js — JSON-via-text via /developer/ai endpoint (no tools/tool_choice
since runAiGeneration silently drops them); recomputes verdict band on
score-mismatch correction so verdict + score stay internally consistent
* lua-chat.js — Ada chat proxy, forwards ?channel= query so the per-call
session pattern (eval-/enrich-/lead-/cta-<uuid>) survives the proxy hop
* slack-notify.js — webhook proxy
* fetch-jd.js — URL scraper with SSRF guard (loopback/RFC1918/AWS metadata)
- Shared origin allowlist in api/_lib/origin.js — reads ALLOWED_ORIGINS env
plus Vercel's VERCEL_URL / VERCEL_BRANCH_URL / VERCEL_PROJECT_PRODUCTION_URL
- vercel.json: SPA rewrites for /results and /score, security headers
(X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
- index.html: remove hardcoded LUA_API_KEY/LUA_AGENT_ID, flip all endpoint URLs
to /api/*, drop client-side Authorization headers (proxy attaches Bearer),
delete dead scoreViaAda + extractScore helpers
- Delete netlify.toml + netlify/functions/*
Vercel Pro gives 60s function timeout (vs Netlify Pro 26s / Free 10s) which
removes the timeout pressure that motivated the earlier browser-direct revert.
Required env vars on Vercel (Production + Preview):
LUA_API_KEY, LUA_AGENT_ID, SLACK_LEADS_WEBHOOK_URL, ALLOWED_ORIGINS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- score-jd.js: call Ada's chat API (/chat/generate) not developer endpoint,
unique eval-{uuid} channel per request prevents session contamination
- lua-chat.js: accept channel from request body for session isolation
- index.html: remove LUA_API_KEY + LUA_AGENT_ID from browser; all Ada calls
now go through /.netlify/functions/lua-chat (server-side credentials);
remove scoreViaAda(), adaSessionUrl(); add newChannelId() helper
- src/index.ts: embed full scoring rubric in Ada's persona so she scores
directly on 'Score this job description:' messages (no score_jd tool call);
remove scoreRoleSkill from skills array
- scrape-jd.skill.ts: add SSRF guard blocking RFC1918, loopback, link-local,
metadata endpoints; restrict to https:// only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Project's package.json has 'build: tsc' for the Lua skill workflow. Vercel auto-runs npm run build on deploy, which tried to invoke tsc (failed with permission error in their build env, but also unnecessary — the web deploy is static HTML + api/ JS, no TS compilation needed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Adopted Vercel hosting (api/ functions, vercel.json) from lua remote - api/score-jd.js: updated to call Ada's chat API (/chat/generate) instead of the developer AI endpoint — Ada scores directly using her persona rubric - Conflict resolution: Vercel /api/* endpoints, adaSessionUrl() channel pattern, removed client-side LUA_API_KEY/LUA_AGENT_ID and netlify functions - src/index.ts: Ada's persona now includes full scoring rubric; scores directly on 'Score this job description:' messages (no score_jd tool call) - scrape-jd.skill.ts: SSRF guard blocking RFC1918, loopback, link-local, metadata Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Netlify redirect /api/:name -> /.netlify/functions/:name so index.html's
/api/* calls work on both platforms simultaneously:
- netlify/functions/score-jd.js: Ada chat API, unique eval-{uuid} channel
- netlify/functions/lua-chat.js: channel from ?channel= query string
- netlify/functions/fetch-jd.js: URL scraper with SSRF guard
- netlify/functions/slack-notify.js: webhook proxy
- netlify.toml: redirect rule + static file publish + security headers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-deployed the agent to restore the inline-rubric persona + skill set after an accidental overwrite from a stale local push. Persona v20, capture-lead v1.0.18, submit-cta v1.0.16, ada-chat v1.0.13, brief-wizard v1.0.13, scrape-jd v1.0.14. score-role correctly absent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without an explicit outputDirectory, Vercel defaulted to public/ once a buildCommand was set — so index.html (at the repo root) was 404'd while api/* functions deployed fine. Point output at . so the static SPA is served. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…dlers Hardens score-jd, lua-chat, fetch-jd, and slack-notify against abuse for the 60k-user launch. The existing isOriginAllowed check only stops casual browser abuse — curl with a forged Origin trivially bypasses it, and an unbounded loop against score-jd / lua-chat burns billable Sonnet tokens. Per-IP limits (stricter of two buckets wins; 429 with Retry-After + JSON body): - score-jd: 5/min, 30/hour - lua-chat: 10/min, 60/hour - fetch-jd: 5/min, 30/hour - slack-notify: 10/min, 60/hour Implementation: ZSET-based sliding window in @vercel/kv, atomic MULTI/EXEC (ZREMRANGEBYSCORE + ZADD + ZCARD + PEXPIRE + ZRANGE) per request. Defensive parsing tolerates both @upstash/redis ZRANGE WITHSCORES return shapes. Fails open (allowed:true) if KV env vars are missing or KV throws — better to serve a real user than 500 the whole site over a rate-limiter outage. Deploy prerequisite: connect a Redis store to the Vercel project via the Marketplace (Vercel KV was retired Dec 2024 and migrated to Upstash Redis; the @vercel/kv client still works, and Upstash injects KV_REST_API_URL + KV_REST_API_TOKEN automatically). Without these env vars the limiter no-ops. Note: api/*.js handlers may merge-conflict with Session C if both land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shareable writeup of the phantom rares@heylua.ai bug: symptom, root cause, the real-lead gate (v1.0.34), and the follow-up moving Data.create below the gate (v1.0.35, pending deploy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The score-time call no longer records to Data, so update the tool
description and skill context: Data write + Slack/Sheets/email happen only
for a genuine lead-form submission; score-time and flagged calls return
{ skipped } and do nothing else.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the deployed version and marks the bug_fix.md follow-up as live. v1.0.35 moves Data.create below the real-lead gate so score-time auto-calls no longer write placeholder records to the evaluations primitive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add §13 covering the rares@heylua.ai bug, root cause, the two-part fix (v1.0.34 real-lead gate + v1.0.35 Data.create move), and the live state. Update the §4 capture_lead description to reflect the gating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thin-JD enrichment chat now asks about judgment_complexity, regulatory_burden, relationship_depth, and system_integration — the four dimensions that previously got no direct user signal — as one numbered list, re-asking only the gaps (4-turn cap), then folds the answers into the JD context for score_jd. - index.html: rewrite enrichment seed (batch of 4, re-ask gaps); label folded answers by dimension in buildEnrichedJd; refresh s-enrich copy; render Ada messages with preserved line breaks (pre-wrap) and **bold**. - src/index.ts: add a scoped "intake / clarification mode" persona carve-out legitimizing the questioning; leave the score_jd / capture_lead / submit_cta hard triggers and their "never ask" rules unchanged. - docs/fixes/ada_persona.md: plan/writeup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring Ada's chat back to the first screen (reverted in d1f3d9a) but wired to the current enrichment brain instead of the old generic one that shipped with 2be603c: - s-hero is now an inline Ada chat (greeting + composer); the paste textarea / dropzone and the separate s-enrich screen are removed. - The thin-JD (<20 words) branch calls the existing startEnrichChat() so the 4 high-leverage dimension questions, **bold**/line-break rendering, and the dimension-labeled JD fold remain the single source of truth. - Detailed JDs (>=20 words) get a quick ack, then go straight to the calibration questions. - Chat panel enlarged (260px -> 440px, wider column, larger text) per the "make it bigger / cleaner" UI ask; Sample/PDF are icon-only hover-expand buttons inside the composer; sub-headline trimmed. - loadSample()/handlePdfUpload() retargeted to the composer; enrichEnterSend rewired to sendHeroMessage; dead handleScoreClick()/sendEnrichMessage() removed; renderScreen resets the chat on s-hero (Back/restart) and on boot. - Fix restart() null-ref now that #otherRoles is gone; drop the §12 redeploy marker since the redesign is shipping again. Plan: docs/fixes/UI_change.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Iterate on the re-shipped chat-first hero per design feedback: - Remove the "Free · No signup" eyebrow and trim the sub-headline; shift the whole hero block up (margin-top: -40px). - Merge the messages area and input into ONE bordered chat panel (avatar on the left). Reply area is a fixed 300px scroll region; input ~90px below it. - Turn the input into a rounded pill that floats inside the panel: round hover-expand tool icons (sample / PDF) on the left, text in the middle, and a circular up-arrow (↑) Send button on the right. - Auto-grow the composer with its content line-by-line up to a 120px cap (COMPOSER_MAX_H), then scroll; collapse back on send / Back / restart, and grow to fit PDF-extracted text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add §15 covering the feat/ui_changes re-ship: the chat-first hero merged with the §14 4-dimension intake brain, the single-panel pill composer (hover-expand icons + circular ↑ send), the auto-grow behaviour, and the flex/height gotcha. Mark §11 as superseded and point it at §15. Also folds in the pre-existing §14 intake-mode doc edit that was sitting in the working tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Shift the hero conversation box 10px→30px left (translateX on .hero-chat). - Enrichment seed (startEnrichChat): Ada now checks whether the brief description already names a job title; if not, question 1 becomes "What is the job title…?" and the four dimension questions follow as 2–5 (five total), otherwise just the four. Judged from the JD preview, no JS detection; the 4-reply cap is unaffected. - Document the conditional 5th question in §14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…implementation - plan.md: scoped roadmap for 4-feature email + CTA landing slice - public/: track logo + social assets used by rendered report email - src/skills/capture-lead.skill.ts: full renderReportHtml inline email body - email-preview.html: align preview with renderReportHtml output
Read ?cta=lua|tech_safari on load and land directly on the matching CTA screen (Lua -> s-connect, Talent Safari -> s-brief), prefilling the lead from name/email/company query params. `cta` alone picks the screen; `rec` is intentionally unused (Option A). role/score are stashed into state.result for submit_cta's Slack-ping context. Also preserve the query string in the initial history.replaceState seed (line 905) so the deep-link isn't stripped back to '/' before the boot handler reads it. Frontend-only: no markup/CSS changes to the landing screens, no new env vars. Implements plan.md §5 F1-F5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(cta): deep-link report-email CTAs into landing screens
- capture-lead.skill.ts: switch email logos (TS, Lua, 3 social icons) to MIME CID inline attachments so Gmail renders without "display images" click. Five attachments via Resend `attachments` field. - email-assets.ts: new file holding base64 PNG payloads for the 5 logos. - submit-cta.skill.ts: F1 fix — gate execute() with no_evaluation_context check; reject calls where scoringResult is fabricated (role_title='Unknown' or score=0). Updates tool description + skill context with explicit precondition. - changes.md: Phase 3 history appended.
Auto-managed version bumps written by the lua push step ahead of the production deploy (capture-lead 1.0.36, submit-cta 1.0.32, persona/backup v47). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ersona v39) State manifest update from the production `lua deploy all` of plan B6 — production now renders the report email with CTA deep-links. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Email implementation fixes
| }); | ||
| const data = await genRes.json(); | ||
| clearTimeout(fetchTimeout); | ||
| return res.status(genRes.status).json(data); |
There was a problem hiding this comment.
Fallback parses non-JSON upstream
Medium Severity
After the streaming path fails, the /chat/generate fallback always calls genRes.json() with no content-type or parse guard, so HTML gateway timeout pages (the failure mode streaming was meant to avoid) throw and surface as a generic 500 instead of a structured error.
Reviewed by Cursor Bugbot for commit 416cf3b. Configure here.
| if (done) break; | ||
| agg.push(decoder.decode(value, { stream: true })); | ||
| } | ||
| return agg.finish(); |
There was a problem hiding this comment.
Stream decoder never flushed
Low Severity
aggregateStreamResponse decodes chunks with { stream: true } but never performs a final TextDecoder flush after the read loop ends, so UTF-8 code points split across the last chunk can be dropped from aggregated text and tool payloads.
Reviewed by Cursor Bugbot for commit 416cf3b. Configure here.
Real lead-form submissions with short JDs were being skipped by capture_lead — no Slack body, no Sheets row, no Data record, no report email — because the gate short-circuited on flags.short_jd. The flag is LLM-judged and misfired on borderline JDs (a 105-word JD was tagged short), so genuine leads (Benjamin/GG Mac, Nick/SDR, king/Discord CM) were lost while only the browser "evaluation starting" ping landed. Fix #1: drop short_jd from the capture gate; keep non_english / suspected_fake as the spam guard. Short but real JDs now post to Slack, append to Sheets, write to Data, and send the report + follow-up email. Fix #2: make short_jd deterministic — recompute from an actual word count (<80) in the score_jd wrapper and overwrite the LLM value, killing borderline false positives. short_jd is still computed and surfaced as a tag (Slack "⚠️ short JD", Sheets shortJd column) for downstream eyeballing. Persona / tool description / skill context updated to match. Phantom-lead protection (no_lead_details gate) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix/short jd lead capture
Add §16 covering the capture_lead short_jd gate fix + deterministic short_jd, and update the §4 skill notes, scoring-model flags line, and §10 gotcha so "never post on a flag" reflects spam-flags-only behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the Meta Pixel base code (id 1022960976820763, PageView) in <head> and fire funnel events from JS as the SPA advances through screens, since the pixel can't observe .screen transitions on its own: - StartEvaluation (custom) when scoring kicks off (runAnalysis) - ViewContent on first verdict reveal (not re-fired on Back-to-verdict) - Lead on the email-gate submit, alongside the existing LinkedIn fire - BriefTalentSafari / ConnectLua (custom) + Contact on CTA submit, distinguishing the human vs agent path Events go through guarded fbTrack/fbTrackCustom helpers (typeof + try/catch) so a blocked or absent pixel script can never break the app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
added meta tracking and docs update
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d338383. Configure here.
| headers: authHeaders, | ||
| body: payload, | ||
| signal: controller.signal, | ||
| }); |
There was a problem hiding this comment.
Stream errors trigger second agent run
Medium Severity
When the primary /chat/stream call fails or errors after the upstream may have started work, lua-chat falls through to /chat/generate with the same payload. That can run Ada twice for one user action, risking duplicate scoring and duplicate capture_lead side effects.
Reviewed by Cursor Bugbot for commit d338383. Configure here.


Note
Medium Risk
New server-side proxies guard secrets and add origin/rate limits, but scoring and lead paths now depend on streaming aggregation and fail-open rate limiting; misconfiguration or stream parsing bugs could break evaluations or allow abuse.
Overview
This branch adds a Vercel serverless
/api/*layer so the static frontend can call Ada without exposing secrets:lua-chatproxies Lua with server-sideLUA_API_KEY, prefers SSE/chat/streamaggregated back to the legacy{ text, steps }shape (fallback to/chat/generate), plusfetch-jd(HTML→text with SSRF blocks) andslack-notify. Sharedoriginallowlisting and per-IP rate limits (Vercel KV, no-op locally) guard all three routes.Local dev is aligned via
dev-server.mjs(npm run devon port 3000).vercel.jsonsets SPA rewrites, security headers, and a 120s function budget for long scores.Repo hygiene: removes committed
.claude/settings.local.json(had approved commands referencing secrets) and expands.gitignore(.env, Claude settings, mock scripts).build.mdis rewritten to match the shipped static + proxy architecture; largechanges.md,docs/llm.md, fix writeups,sheets-apps-script.gs,email-preview.html, and brand assets underpublic/document and support lead logging and email work elsewhere in the branch.Reviewed by Cursor Bugbot for commit d338383. Bugbot is set up for automated code reviews on this repo. Configure here.