* feat(ats): add ATS checker and replace resume analysis
Adds a public, browser-only ATS checker at /ats-checker and an ATS Check
section in the builder's right sidebar. PDFs are parsed locally: text
extraction, reading order, contact and date recovery, section detection,
and file-level readability are scored deterministically, with evidence
cited per finding and skipped checks reported rather than counted as
passes.
Removes the AI-scored resume analysis it supersedes: the resume_analysis
table (dropped via migration), the get_resume_analysis MCP tool, and
POST /ai/analyze-resume. The replacement, POST /ai/ats-review, reviews
extracted resume text and returns qualitative feedback with no score.
Also bumps the version to 5.2.9 and adds the changelog entry.
* chore(deps): bump workspace dependencies
* fix(ats-checker): keep negation inside each 'what this does not do' bullet
The three bullets were bare fragments whose negation came from the
section heading, which translators never see. A dozen locales rendered
them as affirmative assertions or imperatives, so the page claimed the
checker enforces a one-page rule and predicts rejection -- the opposite
of the source, and directly contradicted by the sentence beside it.
Each bullet now carries its own negation, so the polarity cannot be
lost in translation. Re-extracted and refilled across all 53 target
locales.
Postgres defaultNow() stores microseconds while JS Dates are millisecond-truncated, so the SQL equality guard matched zero rows on freshly created resumes and every guarded agent patch failed with a permanent version conflict. The SELECT ... FOR UPDATE lock plus the in-transaction ms-precision check already provide the guarantee; drop the SQL predicate. Verified A/B against a live database.
* docs(adr): propose agent AI SDK v7 adoption plan
* fix(ai): bind analyzeResume through aiService in service test
The test destructured analyzeResume as a named export that does not exist; main was red.
* test(agent): keep pure ai helpers real via spread-actual mock factory
* feat(agent): add run guards, patch version guard, run wall-clock timeout
* feat(agent): validate UI messages at the send boundary
* feat(agent): crash-safe draft-row persistence and server-side cancellation
* feat(agent): reap stale run claims at boot, on send, and on thread open
* feat(agent): fresh-document patch output and tiered context pruning
* feat(ai): shared agent tool contracts and message metadata schema
* feat(agent): add per-thread review-patches setting with update endpoint
* feat(agent): gate resume patches behind hmac-signed tool approval
* feat(agent): merge question answers and approval decisions before run claim
* feat(agent): approval ui with composed auto-send and fixture-driven tests
* feat(agent): usage metadata, tool activity cards, smoother streaming
* feat(agent): tool-call repair, input examples, structured step logging
* chore(i18n): translate new agent workspace strings across all locales
* fix(agent): gate stale-run draft cancellation on winning the claim clear
Snapshot streaming drafts before the conditional clear and skip the flip entirely when another reaper or a replacement run already cleared the claim. Also address review nits in eleven locale catalogs.
* fix(agent): flip reaped drafts only when their snapshotted state is unchanged
* fix(agent): address review findings across run lifecycle, context budget, and approval flow
- bind patches to the revision the model read via signed baseUpdatedAt
- claim the run before consuming a continuation; recorded-but-unexecuted approvals retry as pending continuations
- keep run ownership on stop() until cancellation persists; preserve the claim for the reaper when final persistence fails
- estimate tokens without serializing binary attachments (tokenx) and enforce the budget by dropping oldest whole turns
- mark crash-recovered patch results as snapshot boundaries; strip /data prefixes at execution time
- retry failed continuations without regenerate; mount a single AgentChat; disable response controls on read-only threads; freeze review toggle during runs (client+server)
- accumulate usage across continuations and match the SDK's nested usage shape; label-form token strings; reorderable source label; accessible note field; state-neutral web-search label
* chore(i18n): translate revised agent strings across all locales
* fix(agent): harden baseUpdatedAt validation and address review follow-ups
- bundle tokenx in the server runtime dependencies (e2e boot failure)
- strict ISO schema for baseUpdatedAt plus loud executor rejection of unparseable values
- it-IT source label consistency (Fonte)
- prove penultimate-turn retention in the context pruning test
* chore(deps): exempt tokenx from knip for the externalized server bundle
Fetching an arbitrary job URL server side meant owning SSRF defence, redirect
and size limits, and per-site scraping quirks. The autofill tool now takes only
pasted text, so the URL input, the fetch path and its MCP annotation are gone.
The sheet gates the call behind a tested AI provider and a minimum paste length
so a stray snippet does not spend an AI call.
Public resume pages only produced their OpenGraph and Twitter tags client side,
so a shared link had no card at all. The server now injects them into the shell
and swaps in the resume's own title and description.
The lookup is scoped to public, password-free resumes and deliberately avoids
resumeService.getBySlug: that counts a view and would expose a protected
resume's summary to an unauthenticated crawler. User-authored values are escaped
before they reach the HTML, and any lookup failure falls back to the plain shell.
getResumeSocialMeta is shared with the client route head so the two cannot drift.
Stopping (or archiving) an agent run called controller.abort("USER_STOPPED")
with a plain string reason. The AI SDK only recognizes a cancellation when the
reason is an AbortError (err.name === "AbortError" / isAbortError); a bare
string is treated as a real stream error, and its rejection escaped the
background resumable-stream pump and crashed the whole server process with
ERR_UNHANDLED_REJECTION on every user Stop. Abort with a DOMException named
AbortError (label preserved as the message) so the SDK cancels the run
gracefully. Same fix for the USER_ARCHIVED path.
Claude-Session: https://claude.ai/code/session_01ULhhLQ24DvnYwzP4afDuye
Add comprehensive Application Tracker REST and MCP coverage, document the MCP workflow, add Markdown/ActionLint checks, bump the release version, and fill all extracted translations.
* feat(applications): job application tracker with AI copilot
Add an Applications module at /dashboard/applications: pipeline board
(dnd-kit), table view with bulk actions, Insights (fit tiles, funnel,
sources, shareable funnel-flow SVG), campaigns, tags, CSV import, and
Add/Edit/Detail slide-overs. Each application links a live Reactive
Resume.
AI "Application Copilot" (applications.ai.*): job-posting autofill,
resume↔job match score (fit ring), resume tailoring, and cover-letter /
follow-up drafting — via the user's configured provider.
Board cards + table rows get context menus (edit / move / archive /
delete). Charts are CSS/SVG (no new chart dep); adds a UI Checkbox.
Also includes local TanStack devtools setup and toolchain bumps.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* feat(applications): close follow-up gaps + squash migrations
Finish the deferred/open items on the applications tracker:
- Cover-letter upload re-enabled. Fix the storage blocker by deriving the
key extension from content type (buildFileKey/EXTENSION_BY_CONTENT_TYPE)
instead of hardcoding .jpeg, so PDFs serve correctly and non-JPEG image
avatars keep working under FLAG_DISABLE_IMAGE_PROCESSING. Add
coverLetterUrl/coverLetterName columns + Documents-section upload/remove.
- Contacts editor in the detail sheet (add/edit/remove, keyed per app).
- Board caps rendered cards per column (COLUMN_PAGE_SIZE=50 + "Show more").
- Extract new Lingui messages across locales.
- Guard coverLetterUrl to http(s)/relative at the API boundary.
Squash the five branch-only application-table migrations (create -> +tags
-> +cover-letter -> drop -> re-add) into a single clean CREATE TABLE via
drizzle-kit generate.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* chore: update dependencies
* fix(web): address React Doctor findings — compiler, purity, query, component structure
prefer-module-scope-pure-function: hoist buildSubtitle, getDecimalPlaces,
handleLocaleChange, onLocaleChange, stop, listContent/groupedListContent to
module scope so they aren't rebuilt on every render.
react-compiler-todo (??=): rewrite draft.metadata.styleRules ??= [] to the
non-assignment form to unblock auto-memoization.
set-state-in-effect: derive updatedAtLabel at render time instead of syncing
it through useState + useEffect.
query-destructure-result: destructure useQuery results at call site in
resume-analysis and resume-thumbnail to follow TanStack Query v5 convention.
only-export-components: extract non-component exports to sibling .ts files so
Fast Refresh can preserve component state:
- getNextWeights → typography/get-next-weights.ts
- detectJsonImportType + ImportType → dialogs/resume/import.utils.ts
- getLocaleOptions → features/locale/locale-options.tsx
- preview helpers + DEFAULT_PDF_PAGE_SIZE → preview.shared.utils.ts
- resolveHighlightToolbarState + defaultHighlightColor → rich-input.utils.ts
- computeDelta + getSparklinePoints → statistics.utils.ts
no-multi-comp: split multi-component files into focused companions:
- ResumePane + ToolbarButton → routes/agent/-components/resume-pane.tsx
- DesktopBuilderShell → builder/$resumeId/-components/desktop-builder-shell.tsx
- MobileBuilderShell + helpers → builder/$resumeId/-components/mobile-builder-shell.tsx
- setBuilderLayout/getBuilderLayout moved to -store/sidebar.ts
fix(tests): add Resume type import to section-builder mocks and cast partial
mock data as unknown as Resume to satisfy stricter type checking; fix
noExplicitAny Biome errors in the same mocks.
* feat(applications): improve performance
* chore: fix knip issues
* perf(builder): halve per-keystroke render cost
Section-form fields called `form.handleSubmit()` on every keystroke, which
re-validated the whole form and toggled submit state — firing the render
cascade twice per character (~6809 renders/keystroke, FPS dropping to 9).
Persist via a form-level `listeners.onChange` instead and drop the per-field
`handleSubmit()` (basics, custom-fields, design). Narrow header/dock resume
subscriptions to metadata slices so they no longer re-render on content edits.
Cuts renders 6809 -> 3403 per keystroke (50%), 0 frame drops. Save, preview,
and design controls verified working; 449/449 web tests pass.
* perf(home): eliminate hero CLS from unreserved video box
The hero <section> is `flex items-center` (shrink-to-fit), so the video
wrapper's width depended on the video's intrinsic size, which only resolves
after the media loads. aspect-ratio couldn't reserve height without a definite
width, so the video grew from ~190px to ~563px after first paint and shoved the
centered hero text down ~373px (CLS ~0.095).
Give the wrapper a definite width (w-full + mx-auto on the CometCard) and set an
explicit aspect ratio + width/height on the video so its box is reserved before
load. CLS 0.095 -> 0; hero stays visually centered at max-w-4xl.
* docs: add application tracker guides
* chore(db): squash application migrations
* fix(email): import React in auth template for server-side rendering compatibility
* chore(release): v5.2.1
* Refactor resume rendering and builder workflows
* fix: address application tracker review findings
* feat(export): separate resume/cover-letter downloads, redesign dialog, add Markdown
Let people export the resume and cover letter as distinct documents, and add a
Markdown format alongside PDF / DOCX / JSON (handy for AI agents).
- Server/API: scope PDF generation and download URLs to a resume/cover-letter target.
- Export domain: getResumeExportData + resumeHasCoverLetter in @reactive-resume/resume.
- Redesign the download dialog: one global "What to export" scope toggle (Tabs) plus
flattened per-format rows, reusing existing UI components and design language.
- Add Markdown export (@reactive-resume/resume/markdown) with a small tiptap-HTML converter.
- Fix blank section headings in DOCX and Markdown by injecting the locale-aware
section-title resolver (titles are stored empty and resolved at render time).
- Locale catalogs updated for the new strings.
* test(e2e): open the download dialog before exporting JSON
The JSON export moved into the redesigned download dialog, so the spec now opens
the dialog from the Export sidebar section before clicking "Download JSON".
Removes fs, path, env, getLocalDataDirectory imports from statistics service.
A module-level Map<string, {value, cachedAt}> gives the same TTL semantics
without touching disk. Adds clearStatisticsCache() for test isolation and
updates the test to call it in afterEach instead of relying on unique
LOCAL_STORAGE_PATH temp dirs (finding 5).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Replace 12 near-identical try/catch blocks in threads/actions/attachments/messages
handlers with a single AnyMiddleware that maps the AGENT_ENVIRONMENT_UNAVAILABLE
sentinel to PRECONDITION_FAILED ORPCError.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Some providers (OpenAI Responses API, others) reject system-role messages in
the messages array with AI_InvalidPromptError. Move the analyze/parse system
prompts to the generateText system option, matching the chat handler.
* fix: use non-empty placeholder for redacted resume name
* fix: update stale test title to match new placeholder behavior
---------
Co-authored-by: Albano <alumno26.fazzito.albano@ipm.edu.ar>
Some providers (notably Anthropic via proxies) wrap JSON output in
markdown code fences (```json ... ```), causing Output.object to
throw NoObjectGeneratedError / JSONParseError.
Replace Output.object with manual JSON boundary extraction that works
regardless of fencing. Also propagate the original AISDKError as cause
in throwAiProviderGatewayError for better diagnostics.
Remove attachments and soft-delete the thread before storage cleanup so
partial failures do not leave inconsistent DB state. Log storage errors
without failing the request after the thread is marked deleted.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(ai): remove local AI store now that providers live server-side
The Zustand-based useAIStore has been replaced by the server-side
aiProviders oRPC router (encrypted credentials persisted in DB).
Delete the dead store + tests, drop the ./store export, and remove
zustand/immer deps which are no longer referenced anywhere in
packages/ai/src/.
* feat(agent): archive/delete actions and read-only state for agent threads
- Backend: mark archived threads as read-only in threads.get and reject
messages.send with CONFLICT when the thread is archived.
- Frontend: render archived threads in the sidebar with muted styling and
an Archived badge; add a per-thread dropdown menu in the chat header
with Archive (non-destructive) and Delete (with confirmation); show a
read-only banner above the message list that disambiguates archived
vs. missing-resource causes; suppress the Retry and Stop buttons in
read-only mode.
- Tests: new packages/api/src/services/agent.test.ts covering the
archived-thread isReadOnly flag and the archived-thread send refusal.
* fix(agent): abort run on archive and verify ownership before deleting thread
- threads.archive: before flipping status, abort any in-flight run controller
and clear the active-run state on the thread; cleanup failures are logged
but do not block the status update.
- threads.delete: assert thread ownership via getThread before destructive
work so an authenticated user cannot wipe another user's attachment rows
by passing a foreign threadId.
Adds focused tests for both behaviors.
* feat(agent): display patch diffs and surface revert conflicts
Render apply_resume_patch tool messages with a status-aware card (applied/
reverted/conflicted), expandable operation list, and a Revert button that
correctly handles RESUME_VERSION_CONFLICT responses. Adds unit tests for
the inverse-patch builder and the agentService.actions.revert flow.
* chore(agent): remove out-of-scope attachment tests accidentally added in Task 6
The Task 6 commit (73ef1acca) accidentally re-introduced three attachment-
related tests that belong to a separate task:
- `buildAttachmentModelParts > converts text, image, supported binary, and
unsupported attachments into model parts`
- `agentService.messages.send > persists the user message with file UI parts
and links selected attachments to it` (was failing — the `ToolLoopAgent`
mock is not callable as a constructor)
- `agentService.messages.send > rejects attachments that are missing, foreign,
or already linked before persisting a message`
These were likely re-added during a stash recovery and were not requested
for Task 6, whose scope was limited to the `agentService.actions.revert`
flow. Remove them along with the helpers/fixtures (`buildAttachment`,
`buildActiveThread`, `selectWhereResult`, `selectOrderByResult`) that they
were the only consumers of. `selectLimitResult` is preserved because it is
used by the revert tests.
* chore(agent): configure runtime dependencies
* feat(db): add agent workspace schema
* feat(api): add agent backend services
* feat(web): add agent workspace UI
* chore(agent): remove legacy builder assistant
* test(agent): make agent stream mocks constructible
* chore(web): remove unused resume replacement hook
* feat(api): add unsafe AI base URL flag
* chore(dev): expose local services in compose
* fix(web): normalize resume preview gaps
* feat(api): improve agent tool handling
* feat(web): polish agent workspace UI
* chore: update dependencies
* fix(api,web): address PR review feedback for agent workspace
Security/correctness:
- Restrict AI provider URLs to http/https even in unsafe mode
- Stop exposing Redis on host network by default
- Make .env.local optional and drop app profile in compose.dev.yml
- Store agent attachments with private ACL on S3
- Reset provider test status when provider/model/baseURL changes
- Decouple non-agent AI endpoints from REDIS_URL requirement
- Fix JSON Patch add inverse for existing object members
- Wrap resume patch + agent action insert in db transaction
- Validate partialMessage at runtime and rate-limit attachment uploads
- Add unique index on agent_messages (thread_id, sequence)
UX/bugs:
- Mark agent thread route as ssr: false and guard SSE chunk parsing
- Show config-specific banner only on known configuration error
- Gate AI provider checks behind loading state in resume import
- Fix relative-time formatter blank gap between 45-59 seconds
- Clarify thread delete confirmation message
Polish:
- Raise ENCRYPTION_SECRET minimum to 32 characters
- Bucket AI rate limits by resumeId/threadId/messageId
- Trim form values before submitting AI provider config
- Use single key identifier and nullish-coalesce baseURL display
* fix: address ai agent review feedback
* fix: preserve mobile agent chat state
* docs: add ai agent workspace guides
* feat: introduce design system for Reactive Resume