last conformance run: 3/3 scenarios pass · 18 acts mapped 2026-08-23 01:44 UTC
| Version | 0.1-draft |
| Status | curated draft |
| Curator | Peter Varga |
| Calibration | one person's professional job search, run seriously — not a recruiting platform |
| Provenance | extracted 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.
---
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.
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.
| Entity | Lifecycle |
|---|---|
| company | no 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 |
| contact | relationship cold → contacted → responsive → warm_scout or dead; source mandatory, never cleared |
| posting | lead → evaluated → applied — or skipped (reasoned, kept as pattern memory) |
| application | exactly one per posting: submitted → screening → interviewing → offer → accepted — or rejected / withdrawn / ghosted (all terminal, all honest) |
| interview | rounds under an application; pending → passed / failed / rescheduled / no_show_theirs |
| interaction | immutable touch; may carry the posting's next action — at most one open per posting; a new one supersedes the old, history kept |
| resume_version | registry; applications reference the version actually sent |
12 writes, 6 reads. Acts are named abstractly; implementations publish an act → command map.
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.
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).
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.
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.
| Read model | Must answer |
|---|---|
due | the 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 |
pipeline | the board: every non-terminal item with stage, last activity, open next action, flags |
posting | one posting whole: JD facts, evaluation, its application, interviews, interaction history, open next action |
contacts | people with relationship and last touch |
resume_versions | versions with how many applications each went out on |
check_duplicates | see 4.1 — the guard as a read |
Thresholds behind due (cadence, staleness, ghost horizon, guard sensitivity) are reference data, not code (JH-13).
unknown, not a guess.| Contract | Freedom |
|---|---|
| the 12 acts + 6 reads, required args, refusal semantics | extension commands (config, export…) |
| lifecycles §3, invariants §5 | schema, id formats, guard algorithm details |
| the guard runs on add and its warnings reach the caller | guard sensitivity, similarity measures |
| due-list content (§4.5) | shapes, extra views, FTS |
As for other areas: act map, scenarios under scenarios/ (refusals are contract), mechanically checkable invariants via the platform gates.
| Item | Why |
|---|---|
| Outreach drafting / templates | The area never sends (JH-7); drafting belongs to the session, not the record. |
| Ingestion / scraping | Facts arrive through the hunter or their agent reading real sources — automation of intake is a different product with different failure modes. |
| Offer & comp negotiation modeling | Real, but its own design pass; comp fields carry facts today, not strategy. |
| Multi-hunter | The 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 export | Extension, 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.
| Act | Kind | Required |
|---|---|---|
add_posting | write | title company |
update_posting | write | posting |
evaluate_posting | write | posting |
apply | write | posting |
set_application_status | write | application status |
add_interview | write | application |
interview_outcome | write | interview outcome |
log_interaction | write | direction summary |
complete_next_action | write | — |
add_contact | write | name source |
update_contact | write | contact |
add_resume_version | write | label |
due | read | — |
pipeline | read | — |
posting | read | posting |
contacts | read | — |
resume_versions | read | — |
check_duplicates | read | title |
| Id | Invariant |
|---|---|
JH-1 | Record of reality: facts only from the JD or correspondence; unknown beats guessed. |
JH-2 | Every contact has a source. |
JH-3 | Duplicate warnings are surfaced, never auto-resolved — a person decides. |
JH-4 | One application per posting. |
JH-5 | The end client is first-class; a missing one is visible work. |
JH-6 | One open next action per posting; supersede history kept. |
JH-7 | Suggestions, never actions: the area never sends, submits or clicks. |
JH-8 | Skips are reasoned pattern memory. |
JH-9 | Ghosted is a status, not a deletion. |
JH-10 | Applications record the resume version actually sent. |
JH-11 | Comp is annualized with caveats in prose. |
JH-12 | Interview rounds close with outcomes; outcomes are touches. |
JH-13 | Thresholds are reference data, not code. |
JH-14 | Every write is a logged command with an actor; refusals kept; reads unlogged. |
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.
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | add_posting | ok status="lead" | an agency posting records who the work is actually for (JH-5) — the guard keys on it |
| 2 | check_duplicates | ok warning_count=1 | checking BEFORE adding records nothing — the guard as a pure read |
| 3 | add_posting | ok warning_count=1 | JH-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 |
| 4 | evaluate_posting | ok status="evaluated" | |
| 5 | apply | ok | JH-1: the submission is real, the date was never recorded — 'unknown' is the honest value, a guessed date is corruption |
| 6 | apply | refused Already applied | JH-4: one application per posting |
| 7 | due | ok missing_end_client_count=0 | both agency postings carry their end client, so nothing surfaces here — leave one off and it becomes visible work (JH-5) |
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | add_posting | ok | |
| 2 | log_interaction | ok open_next_action="follow up if silent" | |
| 3 | log_interaction | ok open_next_action="chase scheduling link if none by Friday" superseded_count=1 | JH-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 |
| 4 | complete_next_action | ok | |
| 5 | due | ok open_actions_count=0 | |
| 6 | add_posting | ok | |
| 7 | evaluate_posting | ok status="evaluated" | |
| 8 | evaluate_posting | ok status="skipped" | JH-8: the reason is the value — skipped postings are pattern memory, not trash |
| # | Act | Expect | Why |
|---|---|---|---|
| 1 | add_resume_version | ok | |
| 2 | add_contact | refused source | JH-2: how do we know this person? No source, no contact |
| 3 | add_contact | ok | |
| 4 | add_posting | ok | |
| 5 | apply | ok | JH-10: the application records the exact version that went out — when a recruiter says 'the resume you sent', there is one answer |
| 6 | add_interview | ok application_status="interviewing" | |
| 7 | interview_outcome | ok | JH-12: the outcome closes the round and is itself a touch in the history |
| 8 | set_application_status | ok status="ghosted" | JH-9: silence is an outcome. The row stays; the pattern (which agencies ghost) accumulates |
| 9 | resume_versions | ok length=1 |