// ZPWR-JOBS — JOB-APPLICATION MANAGEMENT PIPELINE // RESUMES + COVER LETTERS + POSTINGS

stryke project · s runmain.stk (AI-draft + render) · renders via use Office · native (no LibreOffice/soffice/python)

stryke-office
// Color scheme

>_ZPWR-JOBS

A private job-application management pipeline built on stryke. One repo holds every application: the resume, each company's posting, the tailored cover-letter prose, and the rendered docx + pdf deliverables. Drop a posting and run s run — it AI-drafts the letter (stryke's ai builtin), renders both formats via stryke-office, and opens the result. Letters can also be hand-written; the render path is pure native Rust — no LibreOffice, no soffice, no python.

What it manages

Identityconfig.toml at the repo root — username (the output-filename token) plus the letterhead: name, two contact lines, and the accent color.
Resumeresume.txt at the repo root — the single source of candidate facts and the visual style every letter matches (serif throughout, cyan #26ADE4 section bars).
Job postingsone per application at jobs/<JOB>/job_desc.txt — the raw posting, kept for reference and as the tailoring source.
Cover letterstailored prose at jobs/<JOB>/letter.txt; rendered to jobs/<JOB>/<Company>_JacobMenke.docx and .pdf.

Layout

zpwr-jobs/
  stryke.toml                    # stryke project manifest ([package], [bin], [scripts])
  config.toml                    # username + letterhead (identity)
  resume.txt                     # candidate facts + style reference
  main.stk                       # `s run` entry — scan jobs, AI-draft, render, open
  cover_letter_pipeline.stk      # the renderer (stryke + stryke-office)
  draft_letter.stk              # AI letter drafter (the `ai` builtin)
  lib/CoverLetter.stk            # pure text + parse helpers (unit-tested)
  t/                             # unit tests — `s test` (stryke only, runs in CI)
  resume.html                    # base resume source          -> pdf
  resume-tech.html               # + per-role tech stacks      -> pdf
  resume-fulltime.html           # full-time roles only        -> pdf
  resume-noselfemployed.html     # minus the self-employed role-> pdf
  resume-nocounts.html           # minus every count           -> pdf
  resume-nocounts-tech.html      # no-counts + tech stacks     -> pdf
  resume-consultant.html         # consulting / architect      -> pdf
  resume-ai.html                 # AI / LLM engineer           -> pdf
  exec-summary.html              # one-page executive brief    -> pdf
  frontend-summary.html          # one-page frontend brief     -> pdf
  clients.html                   # selected projects & work (CV)-> pdf
  render-pdf.js                  # headless-Chrome CDP renderer for every variant
  render-resume.sh               # wrapper -> `node render-pdf.js resume`
  render-cv.sh                   # wrapper -> `node render-pdf.js cv`
  docs/                          # this HUD documentation
  jobs/
    <JOB>/
      job_desc.txt             # the raw posting  (input)
      letter.txt               # tailored letter (input, or AI-drafted)
      <Company>_<user>.docx       # rendered deliverable (output)
      <Company>_<user>.pdf        # rendered deliverable (output)

Architecture

The renderer is one stryke script: four file inputs collapse into a single block model, which stryke-office emits as both formats; a one-page guard gates every write. The render path is pure native Rust — no soffice, no python, no network. Drafting letter.txt with the ai builtin is a separate, optional upstream step (draft_letter.stk); main.stk orchestrates draft + render for s run (see Run it).

Run it

The whole workflow: drop a posting in as jobs/<JOB>/job_desc.txt, then run s run. Bare s run scans every job and brings it up to date — letter missing → AI-draft then render; pdf missing or older than the letter → render; otherwise skip — then opens what it rendered (or, when nothing needed work, the most recently modified letter by mtime). It is idempotent.

# 1) create jobs/<JOB>/job_desc.txt   2) run:
s run                         # process all jobs; open results (newest by mtime)
s run main.stk <JOB>          # one job; opens it even if already up to date

# individual steps (stryke.toml [scripts] aliases; trailing args forwarded):
s run gen   <JOB>             # render an existing letter.txt only (no AI)
s run draft <JOB>             # AI-draft letter.txt only (no render)
s run apply <JOB>             # AI-draft AND render one job
s run gen reuters -f docx -p a4   # flags pass straight through
s test                        # run the unit tests in t/

The renderer requires stryke-office installed (s pkg install -g github.com/MenkeTechnologies/stryke-office). AI drafting uses stryke's ai builtin (Anthropic) and needs ANTHROPIC_API_KEY; apply redrafts shorter (up to 3×) if a draft overflows one page. The s test suite needs only stryke (no stryke-office, no rendering), so it runs in headless CI.

Settings & CLI

Behavior is set in config.toml (parsed by stryke's native toml_decode — keep comments on their own lines, since a trailing # … is kept as part of the value) and overridable per run on the CLI via stryke's getopts builtin (CLI wins). Flags may go before or after <JOB> (short or long); long names match config keys (dashes → underscores). To place a flag first, prefix the args with -- (e.g. … -- -f docx reuters) so stryke passes them through instead of reading the leading dash as one of its own switches. open_after is a negatable boolean: --open-after / -o / --no-open-after.

[generator] verbosity -vquiet | normal | verbose
[generator] formats -fwhich outputs to emit, e.g. docx,pdf or docx
[generator] page -pletter | a4
[generator] open_after -oopen the result when done (macOS open)
[letter] max_paragraphs -Pcap body paragraphs (content length; 0 = no cap)
[letter] max_words -wcap total body words (0 = no cap; over-limit aborts)
[letter] body_size -bbody font size in points
[letter] head_size -Hletterhead/date/recipient/salutation/closing/name size (pt)
[letter] line_height -Lpdf line-height multiplier (× font size); lower to fit a long letter on one page
[letter] margin -mpage margin in inches (pdf)
[letter] subject_prefix -stext before the Subject in the bar (default Re: )
[letter] closing -csign-off line above the name (default Sincerely,)
[letter] salutation -ldefault greeting when letter.txt omits it
[identity] accent -A / ink -kbar fill color / body + letterhead text color
[identity] body_font -B / pdf_font -ddocx serif font / pdf base-14 serif font
stryke cover_letter_pipeline.stk reuters --formats docx --page a4 \
  --max-paragraphs 4 --body-size 12 --verbosity verbose --open-after
stryke cover_letter_pipeline.stk reuters -f docx -p a4 -P 4 -b 12 -v verbose -o
stryke cover_letter_pipeline.stk -- -f docx -P 4 reuters   # flags before JOB

letter.txt format

Key: value headers, then a blank line, then body paragraphs separated by blank lines. Each paragraph becomes one justified block in the letter.

Date: {{date}}
Recipient1: Hiring Team
Recipient2: Mariana Minerals  —  Ann Arbor, MI
Subject: Senior Full Stack Software Engineer
Company: Mariana

I am applying for the {{role}} role at Mariana Minerals …

Second body paragraph …

Any field — header value or body paragraph — may use mustache {{placeholders}}, filled by stryke's stdlib template() from a shared map: every header (lowercased), role (alias for subject), and username/name/address/phone/email/website/github/contact1/contact2/accent from config.toml (contact1/contact2 are the assembled letterhead lines).

Dateletter date line; {{date}} or omitted → today's date (Month D, YYYY)
Recipient1 / Recipient2recipient block (line 1 rendered bold)
Subjectcyan subject bar text, rendered as Re: <Subject>
Companyfilename token — non-alphanumerics stripped → <Company>_JacobMenke.{docx,pdf}
Salutationgreeting line; defaults to Dear Hiring Team,

How it renders

The script builds a structured block model once and emits two formats from it via stryke-office:

docxOffice::doc_write — rich runs (Cambria serif throughout), section bars as a single-cell shaded table (bg => "#26ADE4", white bold run), justified body, US-Letter page.
pdfOffice::pdf_build — absolute layout: cyan rect bars with overlaid white text, base-14 Times body, greedy word-wrap, ASCII-folded typographic characters (bullets → |, em dash → -).

Both come straight from native Rust inside stryke-office — there is no soffice conversion step and no python.

The resume & CV PDFs — a separate path

Cover letters go through stryke-office. The resume and the CV do not: they are authored as HTML and printed by headless Chrome over the Chrome DevTools Protocol. The reason is narrow — Chrome's --print-to-pdf cannot render a page-number footer, so render-pdf.js drives Page.printToPDF directly for displayHeaderFooter + footerTemplate. One script renders every variant.

node render-pdf.js <doc> [--open]
./render-resume.sh [--open]     # wrapper -> node render-pdf.js resume
./render-cv.sh     [--open]     # wrapper -> node render-pdf.js cv

The renderer defines these documents, keyed by the first argument:

resumeresume.html — the base resume, single-column and ATS-clean
resume-techresume-tech.html — the resume plus a per-role tech-stack line
resume-ftresume-fulltime.html — full-time roles only
resume-noseresume-noselfemployed.html — the base resume minus the self-employed role
resume-ncresume-nocounts.html — the base resume minus every product / language / book / invention count
resume-nctechresume-nocounts-tech.html — the no-counts resume plus a per-role tech-stack line
resume-consultresume-consultant.html — consulting / systems-architect angle: engagement model, modernization practice, client-named roles
resume-airesume-ai.html — AI / LLM engineer angle: RAG and chatbot delivery, the ai language primitive, MCP work
execexec-summary.html — one-page executive summary
frontendfrontend-summary.html — one-page frontend summary (web-UI roles + the authored zgui-core library)
cvclients.html — selected projects & work

Each entry carries its own output filename, footer label and PDF DOCINFO metadata (Title / Author / Subject / Keywords). The five variants that share the base resume's keyword surface inherit its Keywords rather than restating them. Metadata is stamped in a second pass through Ghostscript; if gs fails, the un-stamped PDF is kept and a warning printed rather than losing the render.

Geometry is fixed in the script — US Letter, 0.5″ side and top margins, 0.6″ bottom, printBackground on, preferCSSPageSize off. After writing, it reports the page count via pdfinfo and, for the documents that declare an atsCheck string, asserts that the contact line still survives pdftotext extraction — so a layout change cannot silently make the resume unreadable to an applicant-tracking system.

Chrome is launched --headless=new on port 9411 and killed when the render completes. Its binary path is hardcoded to the macOS Chrome bundle, so this path is macOS-only as written. The prerequisites are Chrome, Ghostscript (gs) and poppler (pdfinfo / pdftotext) — none of which the cover-letter pipeline touches.

Add a new application

  1. Create jobs/<JOB>/ and drop the posting in as job_desc.txt.
  2. Run s run. It AI-drafts the missing letter.txt (senior-engineer voice, real resume facts only), renders the docx + pdf, and opens the pdf. Done.
  3. Review the draft. To revise, edit letter.txt and s run again to re-render — or hand-write letter.txt before the first s run to skip AI drafting entirely.

Built on

  • strykelang — the stryke language and runtime.
  • stryke-office — native-Rust office document I/O (docx + pdf rendering).