Skip to content

A run, end to end

One posting, from the moment you press Ctrl+C on a job ad to the folder that holds the PDF. This is resumix clipboard. submit runs the same steps once, for one file, and writes the CV on the main thread before it returns. watch reads postings from a folder and asks only about the CHECK ones — see the client guide.

The call sequence

sequenceDiagram
    autonumber
    actor You
    participant Lis as Listener
    participant In as Intake
    participant Api as HttpApi
    participant Srv as resumix-server
    participant Wp as WorkingProcessor
    participant Gen as CV thread

    Note over Lis: reads the clipboard every 2 s,<br/>listens for q in between

    You->>Lis: copy a posting
    Lis->>In: take(text)
    In->>In: static_jd_guess(text)
    Note right of In: too short, too long or binary<br/>→ printed and dropped, nothing sent

    In->>Api: detect(text)
    Api->>Srv: GET /healthz
    Note right of Api: once per client, 3 attempts, 2 s apart
    Srv-->>Api: 200 ServerStatus
    Api->>Srv: POST /v1/jd/detect
    Srv-->>Api: 200 {is_job_description}
    Note right of In: false → dropped here

    In->>Api: analyze(text, profile, preferences)
    Api->>Srv: POST /v1/jd/analysis
    Srv-->>Api: 200 JDAnalysis
    In->>Wp: hold(text, analysis)
    Note right of Wp: applied, discarded or in flight<br/>→ "already …", nothing written
    Wp-->>In: working/Acme_Corp_Head_of_IT/ (PENDING)

    In->>You: print the analysis, ask
    You-->>In: URL · y · n/s · q
    Note right of In: n/s → discarded/<br/>q → stays PENDING, and stop
    In->>Wp: approve(folder, url)
    Wp->>Gen: queued for its CV
    In-->>Lis: back to the clipboard at once

    Gen->>Api: create_cv(jd, profile, candidate_data, template, images, pages)
    Api->>Srv: POST /v1/cv
    Srv-->>Api: 202 {request_id}

    loop every 4 s until END
        Gen->>Api: cv_status(request_id)
        Api->>Srv: GET /v1/cv/{id}/status
        Srv-->>Api: 200 {status, detail}
        Gen-->>You: [Acme_Corp_Head_of_IT] the step (and detail with -v)
    end

    Gen->>Api: cv_result(request_id)
    Api->>Srv: GET /v1/cv/{id}
    Srv-->>Api: 200 {document, tex, pdf_base64}
    Gen->>Gen: cv_you_head_of_it.json · cv_you_head_of_it.tex · cv_you_head_of_it.pdf

    opt --cover-letter yes
        Gen->>Api: letter(jd, profile, candidate_data, analysis)
        Api->>Srv: POST /v1/letter
        Srv-->>Api: 200 {text, words}
        Gen->>Gen: cover_letter.txt
    end

    Gen->>Wp: deliver(folder)
    Wp->>Wp: log.log, then move → cv/day/Company_Title/
    Wp->>Wp: one row in applications.xlsx

The same run, as state on disk

flowchart LR
    CLIP["clipboard"] --> HELD["working/Acme_Corp_Head_of_IT/<br/>jd.txt · analysis.json<br/>approval_status.txt: PENDING"]
    HELD -->|"y or URL"| QUEUED["working/…<br/>APPROVED, queued"]
    HELD -->|"q: asked again<br/>at the next start"| HELD
    HELD -->|"n or s"| DISC["discarded/26-01-15/…<br/><i>analysis kept</i>"]
    QUEUED -->|"written"| CV["cv/26-01-15/Acme_Corp_Head_of_IT/"]
    QUEUED -->|"a call failed"| ERR["error/26-01-15/26-01-15-09-30-00_…<br/><i>with its log.log</i>"]

A job is assembled under working/ and moved into place in one step when it is finished, so a folder under cv/ is never half-written. Workspace owns every path and every move; WorkingProcessor is the only thing that asks it to make one.

Step by step

1 — The free check runs on your machine. static_jd_guess lives in contracts/ precisely so both sides run the same one: the client uses it before spending a network call, the server before spending a model call. Copying a password or a line of code never reaches the network, and the terminal says so: [clipboard] ✗ not a job description (412 chars, nothing sent).

2 — /healthz before the first real call. A freshly started server (a cold container, a scale-to-zero platform) can take a moment to answer, so the client knocks up to three times, two seconds apart, once per process — not once per call. With -v it narrates the attempts.

3 — Detection costs one small model call. POST /v1/jd/detect answers {"is_job_description": true|false}. Why something was rejected is in the server's log for that request, not in the answer — a caller only ever branches on the verdict. A false verdict on a posting you wanted usually means the copy grabbed only part of the page.

4 — Analysis creates the folder, unless the posting was seen before. The company name and job title in the JDAnalysis are what the folder is named after, so the folder cannot exist before the analysis does. First every analysis.json under cv/, discarded/ and working/ is compared with the new one: a posting already applied to, discarded or still in flight is dropped with the earlier folder's path, and nothing is written. Otherwise jd.txt, analysis.json and approval_status.txt saying PENDING are written at once.

5 — The question is the only thing standing between you and the bill. Everything before it costs one or two small model calls; everything after it costs minutes of the large one. You are asked about every posting, whatever its should_apply says. Answer with the posting URL and it is stored in analysis.json (and in the spreadsheet); y submits without one; n or s files it under discarded/; q stops, and leaves the posting PENDING in working/. --yes answers for you, with the URL the analysis found if it found one.

6 — The CV is written in the background. Saying yes writes APPROVED and queues the folder; the CV thread takes one folder at a time, and the clipboard is read again at once, so you read the next posting while the last one's CV is written. Its lines are tagged with the folder's name, and held back while a question is on screen. q between postings lets the CV being written finish; the approved ones still queued stay in working/.

7 — The CV job is polled, not waited on. POST /v1/cv returns 202 and a request id at once. The client polls /status every four seconds, printing each new step; with -v it also prints detail — the token spend of the step that just finished, and the reviewer's or the page check's own words. When the status reads END it collects the document, the LaTeX and the PDF in one call.

If the client dies mid-job, the job does not: the server keeps writing. Pick it back up with resumix submit posting.txt --resume <request_id>, which skips submitting and starts from /status.

8 — The next start picks up where this one stopped. Anything left in working/ is offered back at startup: resume, clean or quit. Resume sends each folder where its own files say: APPROVED to the CV thread, PENDING to you before anything new is read, and a folder that already holds its document (cv_you_head_of_it.json) is posted to /v1/cv/render instead of starting a job — one LaTeX compile, no model call.

9 — Failure is filed, not printed and lost. Every failure carries a request_id. Once the job has a folder, the client fetches GET /logs/{request_id} for it, folds the server's account into the job's log.log, then moves the job to error/. A detection or analysis that fails has no folder yet: its request id is printed, and resumix logs <request_id> shows what the server did. With --debug the client fetches that log after every call, successful or not.

10 — The spreadsheet is bookkeeping, never a reason to fail. A locked or broken applications.xlsx becomes a warning line in log.log; the CV was paid for and stays delivered.

Where each step lives in the code

Step Module
Reading the clipboard client/src/resumix_client/sources/clipboard.py
The loop: leftovers first, then the clipboard, q client/src/resumix_client/stages/listener.py
The free check, detection, analysis client/src/resumix_client/stages/analysis.py
The question client/src/resumix_client/stages/intake.py, client/src/resumix_client/ui.py
The duplicate check, every move, both queues client/src/resumix_client/stages/working.py
Where a folder goes next client/src/resumix_client/stages/jobfolder.py
Writing the CV, one at a time client/src/resumix_client/stages/generation.py, client/src/resumix_client/cvjob.py
The threads, q and Ctrl-C client/src/resumix_client/stages/watcher.py
Every HTTP call, and nothing else client/src/resumix_client/api.py
Paths, moves client/src/resumix_client/workspace.py
log.log client/src/resumix_client/joblog.py, client/src/resumix_client/stages/terminal.py
applications.xlsx client/src/resumix_client/tracking.py