Changelog
Changelog
All notable changes to gethired will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- Automatic
.envloading in the CLI —python-dotenv(already a dependency) is now wired intogethired.cli. The CLI loads.envfrom the current working directory and the package root on import; existing process env vars always win so command-line overrides still work. Library users importinggethired.tailordirectly are unaffected.
Fixed
- CLI rejected a configured model when only
.envwas set —python-dotenvwas a dependency but never invoked, so users had tosource .envor pass every variable on the command line. Loading happens at CLI import time now. gethired/description.pyoverlay_for_jd— previously deleted when consolidating to programmatic-only; restored as the source of per-JD role/seniority/company/responsibilities for cover-letter production.tests/test_render_pdf.pytectonic + pdflatex test fakes — both indexedcmd[1](the-interaction=nonstopmodeflag) instead ofcmd[2](the tex path), so the fake PDF was never written to the expected location andcompile_pdfraisedAttributeErroronresult.returncode. Tests now readcmd[2]and pass.
Multi-JD + paste fallback + anti-bot recovery
- LLM-driven multi-JD merger (
gethired/merger.py) —merge_job_descriptionsruns a Pydantic AIAgentagainst the configured model and produces a consolidatedAnalysis(union of must-haves, intersection of nice-to-haves, highest seniority, comma-joined companies, deduplicated responsibilities).safe_mergewraps it and falls back to programmaticdescription.consolidateon any failure (rate limit, validation error, missing API key) so the pipeline never blocks on the LLM. The merger runs even for N=1 so every run flows through one code path. - Multi-URL CLI —
gethired run/cover/plan/preflight <url1> <url2> …now fetches every URL, runs the merger, and tailors once against the consolidated analysis. The previouscli.fetch_first_jdhelper that dropped URLs ≥2 is gone. - Per-URL cover letters —
gethired coverwith N≥2 URLs writes onecover_letter_<index>_<slug>.mdper JD. Each letter carries the JD’s own role/seniority/company/responsibilities (viadescription.overlay_for_jd) but shares the merged must-have / nice-to-have / keyword universe. Single-URLcoverwritescover_letter.md(byte-identical to the previous behaviour). - Anti-bot detection + recovery —
fetcher._classify_antibotraises a newAntiBotError(subclass ofFetchError) when a fetch is blocked by Cloudflare (server: cloudflare,cf-ray,cf-mitigated) or AWS WAF (x-amzn-waf-action). The CLI catches it and either launches an inline paste prompt (TTY) or prints the recovery command (gethired run --pasted-jd <file>/--pasted-jd -) and exits 2. --pasted-jd <file>/-flag onrun,cover,plan, andpreflight. Bypasses the fetcher entirely for JDs behind anti-bot walls. Mutually exclusive with<urls>(exit 2). The pasted text is run through the fetcher’s keyword extractor somust_have_keywordsis populated for the merger.- TTY auto-prompt +
--no-tty-promptescape hatch — when an anti-bot block fires and stdin is a TTY, the CLI prompts inline for the JD text and continues.--no-tty-promptshort-circuits to the recovery-command path (useful in pseudo-TTYs and CI). Tailored.master/Tailored.jds/Tailored.analysisoptional fields — the orchestrator attaches these so the CLI can produce per-JD cover letters without re-running the pipeline.- Test coverage — 30 new tests across
tests/test_merger.py(LLM merger + programmatic fallback),tests/test_fetcher.py(anti-bot classification),tests/test_cli_multi_url.py(multi-URL fetching, paste fallback, anti-bot recovery, per-URL cover letters), andtests/test_end_to_end.py(multi-JD run + per-JD cover letter end-to-end). Total suite: 328 passed (up from 297), 1 skipped.
Changed
Tailor.__run_pipeline,plan, andpreflightnow route every JD tuple throughsafe_merge(LLM merger with programmatic fallback) instead of branching onlen(jds) > 1to callconsolidate_analysis. The programmatic consolidator remains the safety net insidesafe_merge.preflight.missing_must_havesreflects the union across every URL (previously: the first JD’s must-haves only).cover_letter.composeis called from the CLI rather than the orchestrator so cover-letter production is the CLI’s responsibility and the orchestrator stays focused on the tailoring pipeline.
Removed
-
gethired.cli.fetch_first_jd— silently dropped every URL after the first. Replaced bycli.fetch_all_jds(returns atuple[Job, …]). - New module
gethired/serialize.py— single source of truth for JSON ↔ domain-model coercion (coerce_master_from_dict,coerce_tailored_from_dict,load_master_from_json,master_to_snapshot,render_json,tailored_to_snapshot_dict,MasterSnapshot). Previously triplicated acrosstailor.read_master_json,audit.__coerce_tailored/__coerce_master, and inline code incli.validate.tailor.pyandcli.pynow re-exportcoerce_bullets/read_master_json/to_tailoredfromserialize.pyfor backward compatibility. - PDF-artefact guard helper —
validator.pdf_artefact_status()consolidates theif pdf_path is None / not existsguard previously repeated ingate_pdf_compiles,gate_pdf_text_extractable,gate_pdf_text_matches_txt, andgate_length_within_limit. Each gate now calls the helper and proceeds with its real check on the happy path. - Property-based tests (
tests/test_normalize_property.py) —hypothesis-driven coverage fornormalise_whitespace(idempotence, no double-spaces),canonicalize_numeric(always returns a set of ints),tokenize_for_overlap(lowercase roundtrip),extract_ngrams(length preservation), andMasterResume.content_hash(determinism, sensitivity to input).hypothesis>=6.100added to dev dependencies. - CLI end-to-end tests (
tests/test_cli.py) —typer.testing.CliRunnercoverage for--help,ingest,show master,show jd(error path),validate(tex vs json, error path),trace(error path),audit(error path),diff(error path),fetch(with monkeypatched retriever). - Module-level
Loggertype alias inobservability.pyfor annotating logger parameters.fetcher.__fetch_with_retrynow takeslogger: Loggerinstead of an untypedloggerparameter. - New
Finalconstants for previously-inlined magic values:TOKEN_ESTIMATE_BASE(2500),TOKENS_PER_BULLET(150),VOICE_DRIFT_NORMALIZER(100),JOB_RATIONALE_PREVIEW_CHARS(100),DROP_REASON_RATIONALE_CHARS(80),KEYWORD_EXTRACTION_LIMIT(40),MUST_HAVE_FALLBACK_KEYWORDS(15).description.pynow exposesRESPONSIBILITY_MARKERS,SENIORITY_KEYWORDS,SENIORITY_RANK,UNKNOWN_ROLE_LABEL,UNSPECIFIED_SENIORITY_LABELas module-levelFinalconstants (the in-functionseniority_rankliteral is gone). - Tri-state ATS gates —
AtsGateResultnow carries astatus(pass/fail/skip);GateStatusandGateTierenums added. PDF-dependent gatesskipwhenLATEX_ENGINE=noneand no PDF artefact exists, so runs without a LaTeX engine are not falsely blocked. - Gate tiers —
AtsGate.tiersplits the 12 gates intoHARD_GATES(9) andADVISORY_GATES(3).AtsGateReport.hard_failed_gates/advisory_failed_gates/skipped_gates;all_passedtoleratesskipandoutcome_from_atsblocks only on hard failures.tailor auditreports the hard/advisory/skipped breakdown. - Compile-based page counting —
gate_length_within_limitmeasures the compiled PDF’s actualpage_countvia PyMuPDF againstMAX_PAGES.
Changed
- AGENTS.md §843-856 visibility compliance — every
self._xsingle-underscore instance attribute has been renamed to its public form (self.x). Affected classes:Tailor(12 attributes),Critic(2),Fetcher(2),Writer(4),Tracer(3),JsonlSink(3),ActiveSpan(3). The class is the encapsulation boundary; attributes are part of the object’s public state. Internal helpers (_xmodule-level) inplain_text.pyandserialize.pyrenamed to public forms too (the_heading_index→find_heading_indexrename avoids a collision with theheading_indexlocal variable inextract_experiencesetc.).tracing.py’s private classes_ActiveSpanand_JsonlSinkare now publicActiveSpanandJsonlSink;_TRACE_PATH_ENVis nowTRACE_PATH_ENV; module-level_new_span_id→new_span_id,_now_iso→now_iso,_noop_cm→null_span_cm. The ContextVar_current_tracerinwriter.pyis nowcurrent_tracerand is part ofwriter.__all__. The module-level double-underscore helpers inaudit.pyandprovider.py(__load_tailored_and_master,__is_minimax) remain “true private” per AGENTS.md. - CLI
__all__andtrace.jsonlschemas unchanged — on-disk snapshots remain byte-compatible because theserialize.pyJSON shape matches the previousrenderer.render_jsonoutput exactly.
Removed
- Dead code:
models.StepTypeenum — the 8-memberStepType(StrEnum)was defined and exported inmodels.py:42-53and__init__.pybut never read by any caller. Removed along with thetest_step_type_enum_valuestest. - Unwired
gethired/search.pyand its three constants (MAX_WEBSEARCH_PER_RUN,WEBSEARCH_PROVIDER_ENV_VAR,WEBSEARCH_DEFAULT_PROVIDER) — theSearchclass was never invoked; Pydantic AI’sWebSearchcapability is not registered in the writer. YAGNI: removed both the module and the orphan constants. The architecture table inREADME.mdnow notes the search sub-agent as “(planned)”. - Local JSON-coercion duplication —
tailor.read_master_json,tailor.to_tailored,cli.master_to_snapshot, the inlinefinalizecoercion,audit.__coerce_tailored/__coerce_master, andrenderer.render_jsonall delegate toserialize.py. - Inline magic numbers in
tailor.py:263,295(2_500 + bullets * 150),tailor.py:311(/ 100),writer.py:222(rationale[:100]),writer.py:543(rationale[:80]),fetcher.py:300(most_common(40)),fetcher.py:320(extract_keywords(text)[:15]),description.py:98-106(seniority_rankdict),description.py:130-138(RESPONSIBILITY_MARKERS),description.py:146-153(SENIORITY_KEYWORDS) — all replaced by named constants (see Added).
Changed
- Breaking: missing contact fields in a master resume now raise
MasterParsingErrorat parse time instead of being silently tolerated. - Breaking:
parse_textis production-ready: stricter handling of math delimiters, multiple education entries, skill category boundaries, and residual TeX commands. - Critic re-runs against the compiled PDF exactly once per run;
merge_critic_jobsdrops all prior validation jobs before appending the authoritative pass. - Fetcher retries now sleep with exponential backoff between attempts; cache persistence uses
dataclasses.asdict. - Writer drops are applied: entries listed in
WriterOutput.droppedare removed from the tailored resume instead of only being recorded. - Removed dead code:
rank_experiences,FINAL_TAILORED_TO_TEXT/FINAL_GROUNDINGaliases, unused constants (MAX_RETRIES,MAX_VOICE_DEVIATION,MAX_BULLET_LENGTH_RATIO,DRAFT_MODEL_ENV_VAR),render_diff, and thenormalizer_helpersshim.
[0.5.0] - 2026-08-02
Added
- OpenTelemetry-compatible tracing in
gethired/tracing.py.Traceremits JSONL spans totailored/<run-id>/trace.jsonl.tracer_for_run()factory; opt-out viaGETHIRED_TRACE_PATH=off. ContextVar-based active span so tool/llm spans emit without threading the tracer through call sites. New module includesTraceSpandataclass withname,kind(agent/tool/llm/validate),started_at,ended_at,duration_ms,attributes,parent_id,span_id. - Deepeval-style agent-evaluation graders in
evals/graders/code.py:- Component layer:
code_tool_correctness(ToolCorrectnessMetric),code_argument_correctness(ArgumentCorrectnessMetric). - Reasoning layer:
code_plan_adherence(PlanAdherenceMetric),code_plan_quality(PlanQualityMetric). - Overall execution:
code_task_completion(TaskCompletionMetric),code_step_efficiency(StepEfficiencyMetric). - All consume the trace.jsonl emitted by the tracer.
WRITER_TOOL_NAMESexposes the agent’s tool set as the canonical reference.
- Component layer:
parse_image()now wires to a vision-capable Pydantic AI agent. Reads the file, sends bytes to a multimodal model named inIMAGE_MODEL(orMODEL), pipes the extracted text through the TeX parser. Thepathargument is now actually used.jobs_from_tool_calls(result)signature simplified: dropped the unusedmasterparameter that was declared for a planned master-aware extraction.
Changed
- Breaking:
job()factory split into focused builders:job_tailor,job_validate,job_lookup, plus the genericjob(). All callers updated. A newJobEnvelopedataclass carries the shared fields (model, tool_name, status, timestamps). Resolves thePLR0913andA002noqa suppressions. - Breaking:
grounding_check()no longer accepts aquantification_thresholdparameter — quantification is enforced bystyle_checkandgate_bullets_quantifiedinstead. - Breaking:
gate_length_within_limit()now uses the structuredTailoredResume(sum of experiences+projects bullets) alongside the TeXre.findallbullet count, cross-checking the two sources. - Breaking:
parse_task()no longer accepts an unusedsourceparameter. - Inline
_bullet/_bulletshelpers incli.pyandtailor.pyreplaced by a module-levelcoerce_bullets()helper. Resolves the visibility-noise introduced by the AGENTS.md §824-856 single-underscore prohibition. evals/harness.pyexception handler retained asexcept Exceptionbut with the per-fileBLE001suppression centralised inpyproject.tomlrather than scattered as inlinenoqa: BLE001comments.
Removed
# noqa:and# type: ignoresuppressions: 9 → 0 acrossgethired/,tests/,evals/. Each suppression was either replaced by a real fix (split factory, dropped parameter, removed dead code) or centralised intopyproject.tomlper-file-ignores. The codebase now passesmypy --strictandruff checkwith zero suppressions outsidepyproject.toml.tests/test_models.py: the no-opif f.default is not f.default_factory or Truefilter that always included every field is gone. The test now actually constructs each model from defaults and verifies frozen semantics.- Defensive
try/except ImportErrorforpymupdf,trafilatura,WebSearch(pydantic_ai) removed — these are now hard dependencies declared inpyproject.toml.
[0.4.0] - 2026-08-02
Added
- PDF compilation via tectonic with pdflatex fallback (
LATEX_ENGINEenv var). New modulegethired/render_pdf.py. Constants:TECTONIC_BINARY,PDFLATEX_BINARY,LATEX_ENGINE_ENV_VAR,PDF_COMPILE_TIMEOUT_SECONDS. New exception:PdfCompilationError. - Multi-JD consolidated run:
Tailor(job_description=(jd_a, jd_b)). Newdescription.analyze_multiple()consolidates analyses (union of must-haves, intersection of nice-to-haves, highest seniority, deduplicated responsibilities). tailor audit <run-dir>: new CLI command + new modulegethired/audit.py. Re-runs grounding, style, plagiarism, and ATS gates against a previous run; emitsaudit.json+audit.md.- Cover-letter tailoring: new models
CoverLetter+CoverLetterParagraph; new modulegethired/cover_letter.py.Tailor(..., produce_cover_letter=True)writescover_letter.md. - Streaming intermediate output: new module
gethired/streaming.pywithProgressEvent+progress_reportercontext manager.Writer.tailor(..., on_progress=...)emits events at step boundaries. --dry-run preflight: new methodTailor.preflight()+ new CLI command. ReturnsPreflightReportwith token estimate, expected gates, JD keyword coverage, voice drift risk, missing must-haves — no LLM call.Tailor.__init__acceptsmodel_instance: object | Nonefor dependency-injected test models.- New CLI commands:
audit,cover,preflight.
Removed
- Deterministic writer fallback.
Writerno longer ships an in-process identity-style transform.Tailor(...)raisesConfigurationErrorat construction whenMODELis unset andmodel_instanceis None.
Changed
- Breaking: callers relying on the silent deterministic fallback must now set
MODEL+API_KEY, or inject aTestModel. The eval harness flagdeterministic: trueis renamed touse_test_model: true.
Fixed
- Repo-wide cleanup: single-underscore identifiers converted to true-private (
__) inwriter.py,tailor.py,fetcher.py,provider.py,cli.py. - Fixed
UnboundLocalErrorinrenderer.render_tex(localenvshadowedenv()function). - Wrapped several lines exceeding the 100-char ruff limit.
[0.3.0] - 2026-08-02
Added
- MiniMax platform provider: bare-name routing for
MiniMax-M3and the M-series;ANTHROPIC_BASE_URLautomatically set to the MiniMax Anthropic-compatible endpoint. gethired/provider.pywithresolve_model()for any provider string.- 9 provider-resolution tests; 2 writer LLM-path tests.
Changed
- Writer now uses a Pydantic AI
Agentwith 7 read-only tools when a model is configured. Falls back to the deterministic writer when no model is set. - All grounding / style / plagiarism / ATS outputs flow through the deterministic validator before render.
Fixed
JobType.LOOKUPenum added so tool-call Jobs render correctly inmatch_report.md.TailoredResume.run_resultmade Optional so Pydantic AI can construct it from the LLM output.
[0.2.0] - 2026-08-02
Added
- Multi-agent architecture: parser, fetcher, description, writer, critic, search, profiler, rubric, validator, renderer, tailor, models, normalize, observability, exceptions, constants, cli. One-word module names per AGENTS.md.
- Tailor orchestrator with
Tailor(resume, job_description, debug, model, draft_model, data_dir, tailored_dir). - CLI (
typer):ingest,fetch,run,plan,show master,show jd,validate,trace,diff. Uniform verb-noun pattern. - Validators: grounding (no fabrication), style (banned-word list with verb-stem matching, parallelism detector, bullet quantification), plagiarism (5-gram overlap minus
TECHNICAL_NGRAMS_ALLOWLIST). - 11 ATS gates:
PDF_COMPILES,PDF_TEXT_EXTRACTABLE,PDF_TEXT_MATCHES_TXT,SECTION_HEADINGS_STANDARD,NO_TABLES_FOR_LAYOUT,NO_IMAGES,NO_COLORS,FONT_SIZE_10_12,LENGTH_WITHIN_LIMIT,KEYWORDS_COVERED,BULLETS_QUANTIFIED,ACTION_VERBS_FIRST. - Traceability:
Jobvalue object (Job.id = uuid4()),Job.description()returningJobDescriptionData;RunResult.jobs;websearch_callsas derived property. - TeX parser handles all resume macros (
\resumeSubheading,\resumeItem,\resumeProjectHeading,\href,\textbf,$O(1)$,R\&D). - Renderer: TeX (Jinja2), plain text, JSON, match_report.md.
- Voice profile builder: avg bullet length, std-dev, opening verbs, punctuation density, sentence count.
- Fetcher: sync httpx, content-hash cache with
CACHE_MAX_AGE_DAYS, retry with exponential backoff. - 67 tests passing.
[0.1.0] - 2026-08-02
Added
- Scaffold:
.gitignore,.env.example,pyproject.tomlwith pinned dependency lower bounds,uv.lock. - Models: 21 frozen dataclasses with
slots=True,WebSearch(renamed fromWebSearchCall),Run.id = uuid4(),SourceReferencewith.description(),job(...)factory. - Normalisation:
canonicalize_numeric(handles10K,10,000+,ten thousand),strip_latex_commands,tokenize_for_overlap,normalise_whitespace,is_action_verb. - Parser against the existing
resume.tex(10 experiences, 3 projects, 1 education, 2 awards). - Central loguru logging via
observability.pywithconfigure_logging(debug, log_file, run_id). - Exceptions:
ResumeTailoringError,GroundingViolationError,StyleViolationError,PlagiarismViolationError,AtsGateFailureError,MasterParsingError,JobDescriptionRetrievalError,ConfigurationError. - 40 tests passing.