Specification · jobhunt @ 0.1

jobhunt — One Person's Search · Area Specification

last conformance run: 3/3 scenarios pass · 18 acts mapped 2026-08-23 01:44 UTC

SpecActsInvariantsScenarios

Spec

Version0.1-draft
Statuscurated draft
CuratorPeter Varga
Calibrationone person's professional job search, run seriously — not a recruiting platform
Provenanceextracted from a production system (huntctrl, an MCP-first job-hunt CRM seeded with a real pipeline); §9 records what stayed behind

The contract all implementations share: acts, invariants, lifecycles, read models. This is a personal area — the subject is one hunter — and deliberately so: Saybooks areas are not limited to corporate functions.

---

1. Calibration: who this is for

A person running a real search: tens of postings in flight, several agencies presenting the same requisitions, interviews in rounds, recruiters who ghost, and a resume that exists in versions. The stakes of record-keeping are personal and sharp: a double submission to the same requisition through two agencies can disqualify the candidate. The system is a record of reality, not a workspace for guesses.

2. Scope

In scope: companies (kind matters: agency vs end client) → contacts (sourced) → postings (lead → evaluated → applied | skipped) → applications (one per posting, through terminal states including ghosted) → interviews (rounds) → interactions (touches + the next-action discipline) → resume versions → the due list.

Deferred (§9): outreach drafting, ingestion/scraping, offer modeling, multi-hunter. Rejected: sending anything, submitting anything, clicking anything. Follow-up output is a suggestion, always.

3. Entities and lifecycles

EntityLifecycle
companyno lifecycle; kind (end_client / consultancy / staffing_agency / product_vendor / rpo) is load-bearing — the dedup guard and the missing-end-client check depend on it
contactrelationship cold → contacted → responsive → warm_scout or dead; source mandatory, never cleared
postinglead → evaluated → applied — or skipped (reasoned, kept as pattern memory)
applicationexactly one per posting: submitted → screening → interviewing → offer → accepted — or rejected / withdrawn / ghosted (all terminal, all honest)
interviewrounds under an application; pending → passed / failed / rescheduled / no_show_theirs
interactionimmutable touch; may carry the posting's next action — at most one open per posting; a new one supersedes the old, history kept
resume_versionregistry; applications reference the version actually sent

4. The acts

12 writes, 6 reads. Acts are named abstractly; implementations publish an act → command map.

4.1 Intake

add_posting (title, company, end_client?, req_id?, url?, jd_text?, comp?, …) Records a JD encountered. Companies are created implicitly by name. Runs the duplicate guard and returns its warnings in the result — surfacing them is contract (JH-3); resolving them is a person's job, never the system's. Freedom: guard sensitivity (reference data).

update_posting (posting, patch) — corrections; comp stays annualized with caveats in prose (JH-11).

evaluate_posting (posting, fit_score?, fit_notes?, resume_version?, red_flags?, skip_reason?) — moves to evaluated, or to skipped when a skip_reason is given; a skip without a reason is refused (JH-8).

check_duplicates (title, company?, end_client?, req_id?, url?) — the guard as a pure read: check before adding, record nothing.

4.2 Applying and progressing

apply (posting, resume_version?, channel?, via_contact?, applied_at?) One per posting (JH-4). applied_at accepts the literal unknown when the submission is real but the date was never recorded — a guessed date is worse than an honest unknown (JH-1). Records which resume version actually went out (JH-10).

set_application_status (application, status, note?) — moves along the pipeline, logs a touch. ghosted is a status, not a deletion (JH-9).

add_interview (application, kind?, round?, scheduled_at?, interviewer?) — a round; moves the application to interviewing.

interview_outcome (interview, outcome, notes?) — closes the round, logs a touch (JH-12).

4.3 Touches and the next-action discipline

log_interaction (direction, summary, posting?, contact?, medium?, at?, next_action?, next_action_due?) The workhorse. A next_action supersedes the posting's previous open one — history kept (JH-6). Direction in/out matters: follow-up nudges key on real outbound touches.

complete_next_action (posting or interaction, note?, direction?, medium?) Closes the open next action; with direction/medium it counts as a real touch, without them it is an internal note that triggers no nudge.

4.4 People and artifacts

add_contact (name, source, company?, role?, …) — a source is mandatory: how we know this person (JH-2). update_contact — source never cleared.

add_resume_version (label, platform?, headline?, focus?, …) — the registry entry applications point at.

4.5 Read models

Read modelMust answer
duethe daily view: overdue and upcoming next actions, follow-up nudges (outbound silence beyond cadence), stale items, suggest-ghosted, agency postings missing an end client, upcoming interviews
pipelinethe board: every non-terminal item with stage, last activity, open next action, flags
postingone posting whole: JD facts, evaluation, its application, interviews, interaction history, open next action
contactspeople with relationship and last touch
resume_versionsversions with how many applications each went out on
check_duplicatessee 4.1 — the guard as a read

Thresholds behind due (cadence, staleness, ghost horizon, guard sensitivity) are reference data, not code (JH-13).

5. Invariants

  1. JH-1 Record of reality. Facts — names, comp, req ids, dates — come only from the JD or actual correspondence. Interpretation is free in fit notes and summaries. A date never recorded is unknown, not a guess.
  2. JH-2 Every contact has a source.
  3. JH-3 Duplicate warnings are surfaced, never auto-resolved. The same req arriving through four agencies is normal; a silent merge or drop invites the double submission that can disqualify the candidate. The guard warns; a person decides.
  4. JH-4 One application per posting.
  5. JH-5 The end client is first-class. An agency posting records who the work is for; the guard keys on it; a missing end client is visible work on the due list.
  6. JH-6 One open next action per posting. A new one supersedes the old; the history of superseded intentions is kept.
  7. JH-7 Suggestions, never actions. The area never sends, submits, or clicks. Follow-up output is advice.
  8. JH-8 Skips are pattern memory. Skipping requires a reason; skipped postings stay queryable.
  9. JH-9 Ghosted is a status, not a deletion — silence is an outcome worth recording.
  10. JH-10 What was sent is recorded. Applications point at the resume version actually submitted.
  11. JH-11 Comp is annualized (USD), with its caveats in prose — "hourly $85 ×2080" is a note, not a hidden formula.
  12. JH-12 Interview rounds close with outcomes, and outcomes are touches.
  13. JH-13 Thresholds are reference data — cadence, staleness, ghost horizon, guard sensitivity live in config, not code.
  14. JH-14 Platform inheritance. Every write logged with an actor; refusals kept; reads unlogged.

6. Contract vs freedom

ContractFreedom
the 12 acts + 6 reads, required args, refusal semanticsextension commands (config, export…)
lifecycles §3, invariants §5schema, id formats, guard algorithm details
the guard runs on add and its warnings reach the callerguard sensitivity, similarity measures
due-list content (§4.5)shapes, extra views, FTS

7. Conformance

As for other areas: act map, scenarios under scenarios/ (refusals are contract), mechanically checkable invariants via the platform gates.

9. Deferred — the extraction record

ItemWhy
Outreach drafting / templatesThe area never sends (JH-7); drafting belongs to the session, not the record.
Ingestion / scrapingFacts arrive through the hunter or their agent reading real sources — automation of intake is a different product with different failure modes.
Offer & comp negotiation modelingReal, but its own design pass; comp fields carry facts today, not strategy.
Multi-hunterThe area's subject is one person; a household or cohort version changes identity, privacy, and every read model. Left until it is real.
huntctrl's markdown exportExtension, not contract — sessions read the record over MCP now.

---

Change log: 0.1-draft amended (2026-08-26) — the guard reads jd_text: a matching job description under a different title (normalized equality or containment, 200-char floor) is the strongest duplicate signal and now warns first, before title similarity — found live when a verbatim-identical JD arrived retitled as a fourth copy of one req. · 0.1-draft amended (2026-08-25) — JH-3's guard falls back to the hiring company as the end client for DIRECT postings, on both sides of the comparison: the original key (end client only) let same-title-same-employer pass whenever req id and url were null, found in live use against real books. check_duplicates gains a company argument. · 0.1-draft (2026-08-23) — extracted from huntctrl (schema, tools, doctrine, and a real pipeline's working practice). The duplicate-guard-as-surfaced-warning (JH-3) and the single-open-next-action discipline (JH-6) are the area's distinctive contributions.

Acts

ActKindRequired
add_postingwritetitle company
update_postingwriteposting
evaluate_postingwriteposting
applywriteposting
set_application_statuswriteapplication status
add_interviewwriteapplication
interview_outcomewriteinterview outcome
log_interactionwritedirection summary
complete_next_actionwrite
add_contactwritename source
update_contactwritecontact
add_resume_versionwritelabel
dueread
pipelineread
postingreadposting
contactsread
resume_versionsread
check_duplicatesreadtitle

Invariants

IdInvariant
JH-1Record of reality: facts only from the JD or correspondence; unknown beats guessed.
JH-2Every contact has a source.
JH-3Duplicate warnings are surfaced, never auto-resolved — a person decides.
JH-4One application per posting.
JH-5The end client is first-class; a missing one is visible work.
JH-6One open next action per posting; supersede history kept.
JH-7Suggestions, never actions: the area never sends, submits or clicks.
JH-8Skips are reasoned pattern memory.
JH-9Ghosted is a status, not a deletion.
JH-10Applications record the resume version actually sent.
JH-11Comp is annualized with caveats in prose.
JH-12Interview rounds close with outcomes; outcomes are touches.
JH-13Thresholds are reference data, not code.
JH-14Every write is a logged command with an actor; refusals kept; reads unlogged.

Scenarios

Each scenario is a file of acts with expected outcomes. ok means the act must succeed with the listed fields; refused means the act must be refused with a sentence containing the listed text. Refusals are contract.

the same req through two agencies: warned, never merged pass

JH-3 (warnings surfaced, never auto-resolved), JH-5 (end client first-class), JH-4 (one application per posting), JH-1 (unknown beats guessed). The area's sharpest rule: a double submission can disqualify the candidate.
#ActExpectWhy
1add_postingok status="lead"an agency posting records who the work is actually for (JH-5) — the guard keys on it
2check_duplicatesok warning_count=1checking BEFORE adding records nothing — the guard as a pure read
3add_postingok warning_count=1JH-3: the same req via a second agency is NORMAL and recording it is right — but the warning rides the result, for a person to act on. Never silently merged, never quietly dropped
4evaluate_postingok status="evaluated"
5applyokJH-1: the submission is real, the date was never recorded — 'unknown' is the honest value, a guessed date is corruption
6applyrefused Already appliedJH-4: one application per posting
7dueok missing_end_client_count=0both agency postings carry their end client, so nothing surfaces here — leave one off and it becomes visible work (JH-5)

one open next action, superseded honestly pass

JH-6 (one open next action per posting; history kept), JH-8 (skips are reasoned), JH-7 (the due list suggests; nothing is sent).
#ActExpectWhy
1add_postingok
2log_interactionok open_next_action="follow up if silent"
3log_interactionok open_next_action="chase scheduling link if none by Friday" superseded_count=1JH-6: the new intention replaces the old one — and the old one is kept as history, because what you MEANT to do is part of the record
4complete_next_actionok
5dueok open_actions_count=0
6add_postingok
7evaluate_postingok status="evaluated"
8evaluate_postingok status="skipped"JH-8: the reason is the value — skipped postings are pattern memory, not trash

rounds, outcomes, ghosts, and what was actually sent pass

JH-10 (the version that went out), JH-12 (rounds close with outcomes), JH-9 (ghosted is a status), JH-2 (contacts have sources).
#ActExpectWhy
1add_resume_versionok
2add_contactrefused sourceJH-2: how do we know this person? No source, no contact
3add_contactok
4add_postingok
5applyokJH-10: the application records the exact version that went out — when a recruiter says 'the resume you sent', there is one answer
6add_interviewok application_status="interviewing"
7interview_outcomeokJH-12: the outcome closes the round and is itself a touch in the history
8set_application_statusok status="ghosted"JH-9: silence is an outcome. The row stays; the pattern (which agencies ghost) accumulates
9resume_versionsok length=1