Skip to content

Fix/short jd lead capture#8

Open
Yash-Lua wants to merge 81 commits into
mainfrom
fix/short-jd-lead-capture
Open

Fix/short jd lead capture#8
Yash-Lua wants to merge 81 commits into
mainfrom
fix/short-jd-lead-capture

Conversation

@Yash-Lua

@Yash-Lua Yash-Lua commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Note

Medium Risk
Touches production API auth, rate limiting, and lead-capture gating; misconfiguration could block legit traffic or change which leads reach Slack/Sheets.

Overview
Adds a Vercel /api/* backend so the static site can proxy Lua chat, JD URL fetching, and Slack without exposing keys: shared origin allowlisting, optional Vercel KV rate limits, and lua-chat streaming (SSE aggregated server-side to the legacy { text, steps } shape, with /generate fallback and long timeouts).

Lead-capture fix (PR theme): capture_lead no longer skips on short_jd—only spam flags (non_english / suspected_fake) block Slack/Sheets/email; short JDs are tagged instead. score_jd overwrites short_jd with a deterministic word count (<80 words) to stop borderline false positives.

Also removes committed .claude/settings.local.json (secrets), tightens .gitignore, adds dev-server.mjs + @vercel/kv, refreshes build.md, and ships a large docs/assets bundle (changes.md, llm.md, fix writeups, email preview, branding PNGs, Lua backup manifest).

Reviewed by Cursor Bugbot for commit e693cbd. Bugbot is set up for automated code reviews on this repo. Configure here.

ReaganKibet and others added 30 commits May 26, 2026 09:26
- 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>
…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>
Platform PR #533 (lua-core) added `structuredOutput: { schema }` to
AI.generate, so the score-role skill can now produce a typed object
directly instead of the JSON-via-text workaround.

- src/skills/score-role.skill.ts: rewrite to use structuredOutput. Drop
  the OUTPUT INSTRUCTIONS block from the rubric (the schema enforces
  shape). Self-correct verdict band on score mismatch. Local type
  extension on AI.generate args (drops once new lua-cli release lands).
- src/index.ts: re-import scoreRoleSkill, restore "Score this job
  description: → call score_jd" persona contract, drop the inline rubric
  Scott had embedded as a workaround.
- index.html: rewrite callScoreApi to route both text and URL paths
  through /api/lua-chat. New extractScoreFromAdaResponse helper walks
  Ada's tool results.
- Delete api/score-jd.js — Ada owns scoring end-to-end now.

Agent push: score-role v1.0.20, persona v21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ReaganKibet and others added 27 commits June 3, 2026 17:10
- renderReportHtml(): email-safe inline-styled HTML for full evaluation
- esc() helper for HTML escaping
- Zod schemas: .passthrough() on candidate objects + scoringResult to preserve extra fields
- sheets-apps-script.gs: add Verdict, Recommended CTA, JD, Analysis columns
- lua.skill.yaml + backup manifest: sync to active version 42
- email-preview.html: standalone render preview

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the paste-box + separate enrichment screen with a single
conversational hero: Ada greets first, the user's first message is the
role (paste or describe), and thin descriptions trigger inline follow-up
questions steered away from the step-2 calibration topics.

- composer is full-width (matches the chat panel); "Try a sample JD" and
  "Upload PDF" are icon-only inside the box and expand on hover; Send sits
  inside at bottom-right
- chat panel is a fixed-height scrolling box with the Ada avatar to its left
- smaller headline, content shifted up, em dashes removed from user-facing copy
- add docs/feature/merge_box_plus_ada.md (design notes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(hero): chat-first intake with Ada leading the conversation
This reverts merge commit 18ad6f1 (chat-first hero intake), restoring
review/latest-updates to the pre-merge state (a9d262e). The change can
be re-applied later by reverting this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a no-op marker comment so the source differs from the identical
a9d262e deployment, which Vercel was deduplicating and skipping. This
forces a fresh production build of the reverted (pre-PR#4) UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
capture_lead is invoked twice per evaluation: once right after score_jd
(to record the evaluation to Data) and again when the visitor submits the
lead form. The score-time call had no real contact details, yet the skill
still posted Slack, appended a Google Sheets row, and sent a report email
using placeholder values (e.g. rares@heylua.ai / "Lua") — producing a
phantom row before every real lead, plus duplicate Slack posts and emails.

Gate the outward signals on genuine lead details (name + title, which only
the lead form supplies); the score-time call now records to Data and stops.
Data recording and the quality-flag short-circuit are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the LLM project guide with the chat-first hero redesign (on
yash/fixes, reverted from prod), the Vercel dedup rollback episode, and
the capture-lead premature-call fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the deployed version after shipping the real-lead gate fix to
production via the gated lua-deploy flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the Data.create below the real-lead gate so the score-time auto-call
(placeholder rares@heylua.ai / "Lua", no name/title) no longer writes a
phantom record to the evaluations Data primitive. Only genuine lead-form
submissions are recorded now.

Note: Data.create now also sits below the quality-flag short-circuit, so
flagged evaluations (short_jd / non_english / suspected_fake) are no longer
stored either.

Co-Authored-By: Claude Opus 4.8 (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>
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>
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>
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
talentsafari Ready Ready Preview, Comment Jun 9, 2026 3:13pm

Request Review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e693cbd. Configure here.

if (done) break;
agg.push(decoder.decode(value, { stream: true }));
}
return agg.finish();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stream decoder missing final flush

Medium Severity

aggregateStreamResponse decodes every SSE chunk with TextDecoder in stream mode but never performs a final decode to flush buffered bytes. Trailing incomplete UTF-8 sequences at chunk boundaries can be dropped, so parsed tool-result lines may be lost and /api/lua-chat can return { steps: [{ toolResults: [] }] } even when scoring succeeded upstream.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e693cbd. Configure here.

Comment thread api/fetch-jd.js
'Accept': 'text/html,application/xhtml+xml,*/*',
},
redirect: 'follow',
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redirect bypasses SSRF URL checks

Medium Severity

fetch-jd validates the submitted URL with isSafeUrl but calls fetch with redirect: 'follow', so a public URL that redirects to loopback, RFC1918, or metadata hosts is still fetched. Initial-host blocking does not apply to later redirect targets.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e693cbd. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants