Job Search Dispatch
A local job-search workspace that turns a captured job ad into an evidence-grounded application draft, with the applicant in control of every decision and submission.
Designed and built end to end · Personal project · 2026 · Private repository
The problem: continuity between finding a job and applying
A job-search workflow crosses several disconnected surfaces: the job board, a record of past work, a model conversation, an application draft and a follow-up list. The difficult part is carrying the right facts and decisions between them without losing the original ad or letting a fluent draft overstate the applicant's experience.
I developed Job Search Dispatch from an AI-generated React MVP into an integrated local application. The current application uses React, Vite and TypeScript, a local Node.js server and a JavaScript Chrome extension. Jobs, My profile and Settings separate daily application work, reusable candidate evidence and model configuration. A ticket keeps the original ad, assessment, application materials and progress together.
One ticket, from capture to review
- 01
Capture the job the applicant is already reading
Fetch from URL covers the common case: a SEEK ad is read by a headless Chromium on the local server, and LinkedIn first tries a credential-free public request. When that fails, the extension reads visible metadata and the complete job description from a matching open tab. Imported text remains editable; the applicant confirms whether the ad is still open.
Job URL → guarded headless fetch or open-tab bridge → editable job ticket
- 02
Keep recommendations separate from application progress
Triage returns structured job details, fit, gaps and a route suggestion using confirmed candidate records. The Jobs list follows original creation order and shows actual application progress with a contextual Continue action. Source and progress filters narrow the list; drafting no longer requires a separate APPLY decision.
Confirmed evidence → assessment → applicant-controlled progress
- 03
Draft with evidence, then check the result
Application drafting uses confirmed evidence explicitly allowed for that purpose. Writer only is the default and must pass deterministic hard checks. An optional Verifier adds a model review; with Repair also enabled, a hard verifier failure permits at most one targeted repair. A result that still fails is not saved as the new letter.
Writer → optional Verifier → validation → optional bounded Repair
- 04
Keep the last mile human
The letter opens as a continuous document. Selecting a paragraph reveals its cited evidence; a separate editor saves changes, and run details expose the model calls and available cost estimates. The applicant reviews and copies clean text for manual submission, then records progress and reminders in the same ticket.
Review → edit → copy → manual submission
Decisions I can defend
Fetch one job at a time, on request, instead of building a crawler
The applicant supplies one URL or already has the rendered page open. A headless fetch restricted to SEEK and LinkedIn, with private destinations blocked and the resolved address pinned, covers the URL; a narrowly scoped extension covers the open tab. Both feed the same Fetch from URL flow, preserving an editable full description and a shared trace ID for failures.
Tradeoff accepted: Headless extraction depends on each job board's current page structure, and the bridge needs the extension installed and a matching tab open. Neither crawls in the background, autofills forms or submits applications.
Make candidate evidence an explicit drafting input
Each evidence record carries confirmation, permitted uses and a claim boundary. Triage matches are intersected with confirmed application-draft evidence before entering the cover-letter prompt. Reusable fragments must resolve to that same allow-list.
Tradeoff accepted: Preparing evidence takes effort. Valid references establish eligibility, not whether every sentence is true; model verification and final human review still have distinct jobs.
Treat model calls as operations that can fail
Stage and trace diagnostics, server-owned deadlines and cancellation make a long-running call inspectable. Failed or cancelled attempts preserve existing work. Writer and any enabled Verifier or Repair receipts explain which calls ran and what cost information is available. Effective routes are validated before drafting and frozen for the run.
Tradeoff accepted: Writer only uses one model call; an enabled Verifier adds a second and conditional Repair can add a third. Roles can follow the preceding route or use their own configured provider. Extra review costs time and tokens, and displayed costs remain estimates rather than invoices.
Evolving the workspace without losing saved work
I migrated the no-build prototype to a bundled Vite frontend and strict TypeScript across the application and server, with shared Zod boundary contracts. Feature modules, state owners and persistence adapters now have separate responsibilities. Existing browser storage keys, saved routes and unknown fields remain compatible; the extension stays JavaScript.
The new interface reduces daily work to Jobs, My profile and Settings. Each ticket has Overview, Cover letter, Application pack and Progress & notes. Reading a letter is separate from editing it, and paragraph evidence stays within reach. Provider credentials and model routing have separate settings pages, while returning to Jobs restores focus without reordering the applicant's records.
The migration preserves the local data model rather than introducing cloud sync. Production startup checks that the bundled output matches its source and rejects missing or stale builds. Profile writes are serialized within a session, but separate browser windows can still overwrite whole-profile snapshots.
What changed in late September 2026
- It now runs as a managed macOS service: a LaunchAgent starts a versioned runtime at login on a fixed local port, and the previous runtime is kept for rollback. Backup and restore were added for the saved workspace.
- Fetch from URL gained the guarded headless path described above, using a pinned Chromium installed inside the project rather than whatever the machine happens to have.
- The Writer can run on a personal Codex or Claude subscription through the official command-line sign-in, alongside bring-your-own-key providers. Subscription routes are limited to the Writer role.
- One declaration lists what each model step reads. A drift test builds every prompt from sentinel data and fails when a prompt and the declaration disagree, and My profile opens on a What AI uses page driven by the same declaration.
- The interface was redesigned with a dark theme that follows the system, and Verifier and Repair are now shown as Fact-check and Auto-fix.
- A ticket can recommend a résumé positioning using local rules and download a locally rendered two-page A4 PDF, flagging the file when its source, template or positioning changes.
- Its evidence model became the starting point for Marshal, which rebuilds the idea for use across more than one device.
A concrete iteration: whose number is it?
A job ad can contain numbers that say nothing about the applicant's achievements. The numeric check needed to distinguish a number present in the ad from one supported by the evidence cited in a letter paragraph. I tightened the check so numeric support comes from that paragraph's cited, confirmed application-draft evidence claims, rather than the job description or unrelated profile text.
I extended percentage checks to require the same quantity and unit in cited evidence: percent and percentage points are distinct, and a bare number cannot support a percentage claim. I also made a failed generation explain its stage and blocking checks instead of leaving the user with a generic failure. Numeric findings identify the paragraph, sentence and unsupported token while keeping the previous saved letter intact. Regression cases cover the source boundary and the failure diagnostics.
This remains a deterministic token check, not a proof of meaning: finding the same number in an eligible claim does not establish that a generated sentence describes it correctly. The optional Verifier can add another review, but final human review remains necessary.
Where the data goes
- Canonical candidate records live in a versioned profile file outside the repository, with owner-only storage and backups. Tickets, provider settings, keys and a capped model-attempt history persist in browser localStorage.
- Model calls leave the machine: the local server sends the selected credential and operation-specific job context plus eligible candidate data to the relevant provider. Local storage does not make generation an offline operation.
- Prompt builders exclude contact details, raw attachments and full extracted résumé text. Operation-specific evidence selection happens in the browser; it is not a separate server-authoritative projection service.
- Attempt history can contain generated personal application text. It retains at most 50 attempts and excludes full prompts and API keys; the user can export or clear it explicitly.
What I verified
For this refresh, I ran npm run verify against an isolated copy of the committed source on 17 September 2026: strict type checking, JavaScript syntax checks and all 371 deterministic tests passed. The production Vite build also passed. Coverage includes evidence eligibility, configurable model roles, percentage support, persistence, cancellation, diagnostics and fixture isolation.
I also ran the current UI journeys against the production bundle at 1440, 390 and 320 pixels. All three passed, covering navigation, paragraph evidence, edits surviving refresh, reminders, profile and settings surfaces with synthetic data and no live model calls.
The project's own release record for 28 September 2026 lists 614 deterministic tests, 40 browser release flows, one backup-restore check, two writing comparisons and three fixed-origin extension checks, all passing with none skipped. The day before, one request to the deployed service extracted a live SEEK ad through the headless path in about 1.3 seconds. I did not re-run that gate for this page, and several older browser suites written for the previous interface are stale and sit outside it.
The portfolio screenshot shows the running application's built-in synthetic fixture data. Its example companies, ticket counts and application statuses illustrate the interface; they are not real applications or outcome metrics. Fixture mode prevents model calls and persistent writes.
Current scope and limitations
- This is a personal local application with a private source repository. No user-adoption, interview-conversion or time-saved result is claimed.
- The verification above does not establish live model quality. One recorded SEEK extraction is not ongoing coverage of SEEK or LinkedIn pages; provider responses and page structures can change.
- Later human edits are not semantically re-verified. Every final claim still needs review before the applicant uses it.
- There is no automatic submission, cloud sync or résumé upload/parser workflow, and it runs on one Mac. The frontend is bundled locally with Vite; model calls and URL ingestion still require network access. The separate Profile v2 experiment remains parked and does not feed drafting.