oll.am · Spec of record · the agentic job scout · 2026-07-10 · v2 layer (§14) 2026-07-11

The agentic job scout — the complete spec.

Reuse the spines · re-aim the OpenCLAW loop · the paid CH job-watch with cited-rationale alerts. The single build-ready spec — it folds the v0 draft, the techniques teardown, and the seam architecture into one canonical source, and states the one architecture decision plus the earliest chargeable slice.
Reuse the spines Cited rationale + abstention Paid multi-user watch No submit tool, ever
This is the spec of record — it supersedes the three sources it consolidates. The techniques teardown studies why each technique is good; the seam architecture proves the scout is a driven pattern, not a new backend; the v0 draft spec set the CH source list and the acceptance test. This page is the one thing to build from — it answers the driving question first (§1: what do we already have vs what else do we need?), then names the one architecture decision, the pipeline, the data model, and the ordered build sequence.
⚡ v2 layer (2026-07-11): the spec now carries a decided second layer — §14: OpenCLAW is the main agent, looping on semantic memory — POC-grounded the same day; start there if you read one section.

1What we have vs what else we need

The engine is not new to invent. Most of an agentic job scout already runs on the oll.am spines — the novel work is a thin slice of new pieces on top. Lead with the honest inventory: everything on the left is live today; everything on the right is the real delta.

REUSE — all live live

  • The OpenCLAW loop — heartbeat → watch → score → dedup → draft → digest → persist, running today as the distribution scout
  • oll-model gateway — cost-capped LLM scoring, provider switch, batch + prompt-cache
  • oll-memory — hybrid dense + keyword + RRF retrieval for the prefilter
  • The oll-mcp agent seam
  • Core — magic-link auth + Stripe subscription = the paid gate
  • Telegram delivery; web_search / web_fetch

WHAT ELSE WE NEED — new build new

  • A structured, versioned candidate profile
  • JSON-LD career-page source-watchers + one aggregator + manual-paste ingest
  • Two-stage matching (embedding prefilter → cited-rationale LLM rerank) + abstention + relevance floor
  • Cross-source near-dup dedup; relevance calibration
  • Tiered digest (apply-grade / watch) + cadence
  • The PAID delta — multi-user + Core Stripe subscription + per-user saved searches + feedback loop
  • Observability / AIOps layer; cost controls; scheduler + state stores
  • The MCP tool surface — no submit tool

The thesis

Don't build a job scout — re-aim the distribution scout on the spines you already own. Schedule-driven watch→score→draft→digest is solved in our own codebase; auth + payment + email are solved in Core; hybrid retrieval is solved in oll-memory; cost-capped inference is solved in oll-model. The delta is a thin service that adds sources, a cited matching engine, and a paid gate.

2The one architecture decision — where it runs

Three honest options. The fork is not "how good is the matching" (§6 answers that everywhere) — it's where state, sources, and billing live. The recommendation is the hybrid.

OptShapeTrade
AOpenCLAW skills onlyFastest — reuses the whole proven loop verbatim. But single-tenant / personal: state lives in the agent's files, hard to bill, no per-user isolation. Great for dogfood, not a product.
BStandalone oll-scout serviceFlask, own Neon DB, Coolify, MCP seam — a proper multi-user paid product. But re-implements scheduling / digest that OpenCLAW already does well, and forfeits the reasoning-runtime the loop gives for free.
CHYBRID recommendedAn oll-scout service owns data / state / sources / billing on the spines; an agent runtime owns the reasoning steps (score / draft) via MCP; the digest + scheduling reuse the OpenCLAW loop's proven techniques. Each layer does the thing it's already best at.

Why hybrid fits a PAID multi-user product

A paid product needs three things a pure-skills setup can't cleanly give: per-user isolation (saved searches, seen-state, feedback keyed off a Core JWT sub), a billing seam (Core's Stripe subscription, checked live from /me), and an auditable data store (every score row persisted with its cited rationale and cost). Those are database + service concerns — so oll-scout owns them.

But the reasoning (rank this posting, draft this digest entry) is exactly what an agent runtime + MCP tools do well, and the cadence machinery (fire-windows, double-fire prevention, smart-split digest) already runs in OpenCLAW. Hybrid keeps the service thin — it's a data / sources / billing spine with the reasoning and scheduling reused, not re-authored.

3The flow — end-to-end pipeline

One scheduled run, ten stages. The two accent-2 bordered boxes are the only genuinely new logic; the rest re-aims the proven loop.

Stage 1TriggerHeartbeat fire-window; double-fire prevented via last_fired
Stage 2 — NEWWatch sourcesJSON-LD career pages + aggregator + manual paste
Stage 3Normalize + content-hashCanonical fields; hash for identity
Stage 4Cross-source dedupMinHash near-dup; canonicalize to ATS
Stage 5Stage-1 prefilteroll-memory embedding sim; drops noise cheaply
Stage 6 — NEWStage-2 LLM scoringoll-model rubric → score + cited rationale + gaps + dealbreakers; honest abstain below floor
Stage 7Tiered digest assembly≥70 apply-grade · 50–69 watch
Stage 8DeliverTelegram + email
Stage 9Persist seen-stateCloses the loop; no repeats
Stage 10Feedback capturethumbs / save / dismiss re-weight the profile
StageWhat it doesOrigin
1Trigger — evaluate the fire-window; skip if already fired this windowOpenCLAW
2Watch sources — JSON-LD career pages + one aggregator + manual pastenew
3Normalize into canonical fields; compute content_hashnew
4Cross-source near-dup dedup (MinHash), canonicalize to the employer ATSnew
5Stage-1 embedding prefilter via oll-memory — drops obvious noise, cheapoll-memory
6Stage-2 LLM scoring via oll-model — rubric → score, cited rationale, gaps, dealbreaker flags; abstain below floornew
7Tiered digest assembly — ≥70 apply-grade, 50–69 watch, sortednew
8Deliver — Telegram + emailOpenCLAW
9Persist seen-stateOpenCLAW
10Feedback capture — thumbs / save / dismiss / appliednew

Budget

Stage 2 (the expensive one) is capped at N postings/day (v0: N=25) plus a daily gateway spend cap — overflow queues to the next run. The prefilter (Stage 5) exists precisely so the frontier model only ever sees survivors: cheap-wide recall first, expensive-narrow last.

4Data model — keyed by user

Six tables. The paid multi-user addition over the v0 draft: everything is keyed by user (Core JWT sub), and each user owns their own saved searches.

TableFields
profileVersioned JSON: narrative summary; evidence map (claim → proof); title-cluster; hard gaps; dealbreakers; preferences
sourceid, type (career_page | api | manual), url, parser, schedule, active, last_run, failure_count
postingid, source_id, url, raw, normalized{title, company, location, pensum, language, description}, content_hash, first_seen, last_seen, status
scoreposting_id, profile_version, prefilter_similarity, llm_score (0–100), rationale[] each citing posting text + profile claim, matched_requirements[], gaps[], dealbreaker_flags[], abstained, model, cost, created_at
digestid, sent_at, channel, posting_ids[], render
feedbackposting_id, user_signal (thumbs | save | dismiss | applied), created_at

The paid multi-user addition

Every row above is keyed by user (Core JWT sub). Each user gets per-user saved searches — a saved search is a (title-cluster + preferences + source-set) bundle that the scheduled run ticks. This is the difference between a personal tool and a product: isolation is a column, not a fork.

5Sources — legal coverage

The matching engine is only as safe as its input. The rule: use what employers and portals publish for machines, and never touch what they contractually forbid.

schema.org/JobPosting JSON-LD — the clean spine

Curated CH employer career pages emit JSON-LD the employer published for Google for Jobs. The required-field contract IS the ingest schema — structured, sanctioned, free.

Why it matters · the employer already structured it for a crawler

The v0 CH list — complete, no additions

AbraxasBedagSwisscomInventxSIX GroupErgonELCAAveniqti&mSwiss Post

Fetched 1×/day, polite: a single request per page, UA identified, robots.txt respected. Adding sources beyond this set is a later milestone, not v0.

One aggregator API for breadth

Arbeitnow (free, no-auth, DACH) as the zero-cost seed; Adzuna or Jooble free tier as the alternative — pick whichever key arrives fastest. Verify each portal's rate limits, CH coverage, and commercial terms first.

Why it matters · breadth with a signed contract, at zero cost

Manual-paste ingestion

An MCP scout_add_posting(url_or_text) tool keeps the value of jobs.ch / portal-alert emails without touching them programmatically — the human pastes, the engine ingests + scores. Zero ToS surface.

Why it matters · portal value without a portal contract

HARD non-goal — NEVER scrape LinkedIn / Indeed

hiQ WON on CFAA but LOST on breach-of-contract — permanent injunction, $500k, company dead. Indeed's ToS bans bots. The safe envelope = logged-out + machine-published JSON-LD + sanctioned APIs; GDPR still applies to any personal data. (hiQ v. LinkedIn · schema.org/JobPosting)

6The matching engine — the differentiator

A keyword alert matches strings; this reasons about fit. Two-stage retrieve-then-rerank, with a scoring rubric that quotes before it judges and is licensed to abstain.

Two-stage retrieve-then-rerank

Cheap embedding recall drops obvious noise (Stage 1, oll-memory), then an expensive LLM rerank runs on just the survivors (Stage 2, oll-model). Precision where it's affordable, recall where it's cheap.

Why it matters · the frontier model only ever sees survivors

Hybrid dense + BM25 + RRF

Fuse dense and keyword with RRF. Keep BM25 for exact tokens — company names, framework versions, certifications — that dense pooling averages away.

Why it matters · exact tokens matter, and dense forgets them

The cited-rationale scoring rubric — the CONTRACT

Typed JSON, contract-tested. Inputs: the normalized posting + the current profile. Output:

  • Overall score (0–100) + a one-line verdict
  • Per-requirement match table — requirement quote → profile evidence quote → met / partial / gap
  • Dealbreaker check (location, language, Pensum floor)
  • "Interview angle" one-liner — what the user would lead with
  • Reason BEFORE the score; small integer scales; structured JSON out
  • Abstain if posting text < threshold or language ≠ DE/EN

Every rationale item MUST quote the posting — no uncited claims. RAG discipline applied to postings: honest abstention over fabricated fit. A scout that lies about fit is worse than none. (Reductions are directional and model-dependent — validate on the real model.)

7MCP tool surface — discover, rank, draft

Partially superseded by §14 (2026-07-11): the personal loop is now driven by OpenCLAW through seven new job verbs on the oll-mcp thin seam; the surface below stays the plan for the paid oll-scout arm.

Six tools, all read or draft. The agent is the primary user; the tools are the seam.

ToolDoes
scout_profile_get()Return the current versioned profile
scout_profile_update(patch)Apply a patch → new profile version
scout_add_posting(url_or_text)Ingest + score immediately; returns the cited score
scout_add_source(url)Validate parser; add inactive pending review
scout_query(question)NL query over scored postings ("public-sector above 75 this week?")
scout_digest_now()On-demand digest
scout_status()Last run, source health, spend vs cap

CRUCIAL — NO submit / apply tool is registered

Discover / rank / draft only. There is no world-changing tool in the set — so the agent cannot auto-apply by construction. Postiz teaches how to gate the one dangerous call (openWorldHint, discovery-then-act, a type enum with draft as the safe default); we take that lesson to its safe extreme and simply don't hand the agent the dangerous call. Assistierend, nie autonom.

8Alerting — that doesn't fatigue

A scout that pings on every posting trains the user to ignore it. The alerting discipline is as important as the matching.

Tiered digest, not per-posting pings

Apply-grade (act now — top slot) vs watch (FYI). Scheduled digest, never per-posting pings — irregular sending drives more unsubscribes than frequent; cadence, not volume, is what burns trust. The user sets the frequency.

Why it matters · scarcity of the top slot IS the signal

Near-dup fingerprinting

One job = one alert, with an honest first-seen date — MinHash + canonicalize to the employer ATS so reworded reposts don't re-alert.

Why it matters · repeats are the fastest way to lose trust

Email default, Telegram for breakthroughs

Email = low-friction universal default + an audit trail. Telegram = opt-in high-priority breakthrough for the apply-grade tier only.

Why it matters · match the channel to the urgency

9The paid gate — what makes it a product

The difference between a personal tool and revenue is one seam: Core owns the money. The service just checks the plan.

Core magic-link auth + Stripe SUBSCRIPTION

A CHF 19/mo Career-Radar tier. Core owns all the Stripe code; oll-scout just checks the plan live from /me (identity in the JWT, entitlement from /me — the house rule). A RADAR_PLAN_GATE requires a non-free plan to create or tick a saved search.

Why it matters · zero Stripe code in the product — Core is the money spine

Per-user saved searches, keyed off the JWT sub

Each paying user owns their saved searches; the scheduled run ticks only active, plan-gated searches. Isolation is a column, not a fork.

Why it matters · multi-user is a key, not a rebuild

The feedback loop

Thumbs / save / dismiss are strong sparse labels that re-weight the profile; the inferred profile stays editable. Clicks are noisy abundant signal — absence ≠ dislike. The scout sharpens the longer it's used.

Why it matters · the product improves per-user with use

Honest note — which one is the stranger franc

The free / personal scout is dogfood + the AIOps portfolio demo — it does NOT count as a stranger franc. The paid multi-user watch + alert subscription is the actual revenue product. Don't confuse the demo with the sale.

10Observability + cost controls

This section IS the interview demo: an agent that runs unattended, cost-capped, self-monitoring, with cited outputs. Two halves — watch it, and keep it cheap.

Self-monitoring — the AIOps layer

Request-ID JSON logs per stage + a per-run summary (sources ok/failed, postings new/scored/abstained, spend). Sentry on parse + gateway errors. A source is auto-disabled after 3 consecutive failures (alert in the digest footer). "Expected data, got zero" (a 200 OK with an empty body) is treated as an ERROR; watch volume-drop vs baseline. Health probe = DB + gateway + last-run freshness < 26h. End-to-end run tracing with token / cost.

Why it matters · a silent-failing scout is the worst kind — it looks fine

Cost gradient — the biggest lever

Cheap-wide recall first (JSON-LD / API ingest + BM25 + dense), expensive-narrow last. A FrugalGPT-style cascade escalates only low-confidence cases; hard-cap the top-N to the frontier model. Batch API (async, 50% off) + prompt-cache the stable rubric prefix (0.1× reads). Tier Haiku → Sonnet → Opus.

Why it matters · the single biggest determinant of whether a scout is affordable to run

11Non-goals — the hard scope fence

  • No auto-apply / no form-filling / no submission anywhere — there is no submit tool, by construction (§7)
  • No LinkedIn / Indeed scraping — the safe envelope only (§5)
  • No more sources than the listed v0 set
  • The free personal tool does NOT count as a stranger franc — it's dogfood

12Build sequence — earliest chargeable slice first

Ordered so the earliest slice that could charge comes early. The paid gate (M3) is the milestone that turns dogfood into a product.

1  M1 — the loop, personal

Profile + one JSON-LD source-watcher + Stage-2 cited-rationale scoring + a digest. Proves the loop end-to-end on a single source, personal.

2  M2 — breadth + quality

The aggregator + cross-source dedup + the remaining sources + relevance calibration.

3  M3 — the chargeable product

The paid gate (Core subscription + multi-user saved searches) + the feedback loop. This is the milestone that could charge a stranger franc.

4  M4 — polish

Observability polish + cost controls (batch, prompt-cache, cascade tiers).

Cut order if time runs short

Cut from the bottom: aggregator → sources 7–10 → scout_query. NEVER cut: citations, abstention, cost cap, no-auto-apply.

13Open decisions — decide fast, no research spirals

Partially superseded by §14 (2026-07-11): run-location and order-of-attack are now decided — OpenCLAW drives the personal loop, oll-scout stays the paid arm, personal-first. The remaining rows below still stand.

DecisionOptions / lean
Product nameNOT "JobScout" (→ JobScout24 collision). Working name oll-scout; pick a real one Saturday.
Run-locationA / B / C — recommend hybrid (C) for a paid multi-user product (§2)
AggregatorArbeitnow (free, zero-auth seed) vs Adzuna vs Jooble — whichever key arrives first
Digest channel defaultTelegram (fast to wire, better mobile) vs email (universal + audit trail)
Order of attackPersonal-first (dogfood, proves the loop) vs paid-multi-user-first (revenue sooner)

14v2 — OpenCLAW is the main agent — the loop on semantic memory (decided 2026-07-11)

Reviewed by the operating agent itself (OpenCLAW "Claw", 2026-07-11) via a gateway agent turn: verdict AMEND — all amendments incorporated below; the plan was approved contingent on the job_update state machine landing before the loop goes live.

The one-liner. The job scout is not a new build — it is three finished experiments connected. OpenCLAW (the live local agent gateway, Telegram-connected) becomes the scout's brain and hands; oll-memory (pgvector hybrid retrieval) becomes its semantic store; the oll-mcp thin seam is the only wire between them. The oll-scout service (PR #94) stays the deterministic, multi-user, PAID arm — same store, different driver.

Why OpenCLAW as main agent wins

  • Coverage was oll-scout's honest gap (zero keyless Swiss sources). An agent with WebSearch/WebFetch/browser IS a source — it searches the whole internet, including employer career pages, without per-board adapters.
  • The judge step costs nothing. OpenCLAW's inner Claude runs on the subscription, so the mandatory LLM-judge gate has zero marginal API cost in the personal loop. (oll-scout's llm_cited scorer via oll-model/Groq remains the scorer for the multi-user paid arm, where per-call cost is priced in.)
  • Alerting already works. Sam has already done job lookups via Telegram through OpenCLAW (@ClawBoiSamBot); alerts are a message send, not a new channel build.

Architecture — one wire, one store

AgentOpenCLAW gatewaycron · WebSearch · inner-Claude judge · Telegram
① tool call · streamable-HTTP · seam JWT
Seamoll-mcp thin seamstreamable-HTTP, host port 5097 — moved from 5099, now occupied by Postiz
② POST /api/memory · X-Service-Token
Storeoll-memorypgvector · hybrid RRF retrieval
③ SQL · embeddings
DBPostgres + pgvectorthree collections per user
Paid arm — M2oll-scout servicedeterministic sources (jooble · arbeitnow · JSON-LD) + llm_cited scorer
④ writes into the SAME memory collections (M2)
ConvergenceOne shared corpusagent sources + deterministic sources land in one place; two drivers, one store

Data model — three collections per user

Collections are derived server-side from the seam's JWT sub — the client can never name another user's corpus. Status flips (new→alerted) are a re-ingest with the same document_id — idempotent, no new endpoint needed.

CollectionContentsDoc idMetadata
mem:user:{sub}:jobsOne doc per posting, full textsha256(canonical url) → repost/reword = same doc, native upsert dedupcompany, location, url, source, posted_date, status: new→alerted→dismissed
mem:user:{sub}:cvCV chunked by section (experience / skills / projects) so matching weights real project evidence over keyword listscv-{section}section
mem:user:{sub}:queriesStanding interest statements ("AIOps Zurich, senior"), themselves embedded so the query set dedupes semanticallyq-{slug}active, created

The loop — the numbered call sequence

  1. Wake. OpenCLAW cron (daily, e.g. 06:30) wakes the agent with a standing prompt.
  2. Read interests. The agent enumerates the live-editable standing interests with ollam_query_list (plus ollam_job_match/ollam_job_list against the queries + cv collections as needed) — never a prompt-frozen list.
  3. Search the open web. Boards it can read + employer career pages of named targets. LinkedIn scraping is explicitly out (ToS). jobs.ch only via its public pages, consistent with the legal stance in §5.
  4. Store with memory. For each candidate posting: ollam_job_store — the seam hashes the URL, upserts to mem:user:{sub}:jobs; an already-seen URL is a no-op and the loop skips it. This is what turns "I looked up jobs on Telegram" into a scout with memory.
  5. Recall gate. ollam_job_match — the seam queries the posting text against the cv and queries collections and returns vector_score per hit. Floor ~0.5 (ollwrite's measured floor on nomic) = the RECALL gate.
  6. Precision gate. The inner Claude judges each above-floor posting with the cited contract borrowed from oll-scout's llm_cited scorer: a per-requirement verdict, quoting the posting AND the CV chunk as evidence; it abstains on thin (<200 chars) or non-DE/EN postings. Measured below — cosine alone cannot carry precision.
  7. Alert. Above-threshold matches → one Telegram message each: title · company · link · a one-line why-it-fits citing the CV evidence; then ollam_job_update(url, alerted, score, one-line verdict).
  8. Draft on demand. On a "draft a cover letter" reply: the existing ollam_draft, grounded by the matched CV chunks — already POC'd.

The standing prompt is a contract, not a vibe — operator-specified, 2026-07-11

  • First line of every scheduled run: ToolSearch for the ollam_* tools before anything else. MCP tools are deferred in fresh sessions; without this the agent silently degrades to ad-hoc notes — the exact failure mode the migration kills.
  • Fixed judging rubric pinned in the prompt so format cannot drift run-to-run: requirement → posting quote → CV quote → match yes/partial/no; explicit abstain triggers (posting <200 chars, non-DE/EN). The 0–100 fit score is the agent's judgment and stays clearly distinct from the 0–1 vector recall floor (noise filter ≠ verdict).
  • Sources are UNRESTRICTED (Sam directive, 2026-07-11 evening — "we should not restrict the sources, give OpenCLAW full freedom, we often don't know what company will have good jobs") — where to look is the agent's judgment call each run, varied day to day: general web, niche boards, career pages inferred from the CV, DACH tech scenes, funding-news-as-hiring-signal. The old seed list (jobs.ch, SIX, Google, Apple, UBS, Swissquote) survives only as a non-exclusive baseline, never a fence. The ONE hard exception stays: no LinkedIn scraping in any form (legal stance, not a source-scope decision).
  • Compounding discovery (operator-designed): a source earns a permanent standing slot — stored via ollam_query_store as a source: note — only when it produces a posting that stores as NEW and clears the recall floor. Good discoveries accumulate run over run; noise sources never make the list.
  • Trade-offs on record, accepted deliberately: coverage becomes non-reproducible (no guarantee any specific board is checked on a given day), the first ~2 weeks run un-scaffolded until source: notes accumulate, and per-run cost rises — discovery over consistency, because an atypical profile's best fits are non-seedable.
  • Zero new postings = stay silent. No "nothing found today" noise.
  • Every alert carries the raw posting URL (one tap to apply), not just the why-it-fits line.

MCP seam extension — seven thin verbs, ~180 lines, zero new backend logic

They wrap the two existing oll-memory endpoints; corpus + acl are derived from the JWT sub exactly like ollam_memory_*.

VerbDoes
ollam_job_store(url, title, company, location, text, posted?)Upsert doc, id = sha256(url); returns stored | already_seen
ollam_job_match(text, top_k?)Queries cv + queries collections; returns per-collection vector_scores + above_floor flag
ollam_job_update(id_or_url, status, score?, verdict_note?)The state machine the daily loop depends on. Status enum: new | watched | alerted | dismissed | applied | interviewing | rejected | expired. Under the hood it is a metadata re-ingest on the same document_id — no new oll-memory endpoint. Why it matters: URL dedup only blocks re-STORING a seen posting, not re-JUDGING it — without status write-back the agent re-judges and re-alerts the same postings every morning.
ollam_job_list(status?, limit?)Recent jobs by metadata filter
ollam_cv_ingest(section, text)Upsert cv-{section}
ollam_query_store(slug, text, active?)Upsert a standing interest
ollam_query_list(active?)Read side of standing queries — the search phase enumerates live-editable interests instead of a prompt-frozen list

ollam_job_match is two retrieval calls + a floor — still a pure adapter, no generation.

Measured grounding — the POC ran today (2026-07-11)

Against the LIVE local stack (poc-oll-memory-1 on :5008 + local Ollama nomic-embed-text). Reported honestly:

  • Flow proven end-to-end: stored the two real AIOps-Zurich postings (Senior Observability Engineer @ SIX Group; Observability & AIOps Consultant @ Digital Architects Zurich) + a control (marketing) + CV + standing query into three collections; lookup returned the AIOps roles above the control.
  • Semantic proof, zero keyword overlap: the query "keeping production systems healthy using machine learning on telemetry data" (shares no words with the ads) → nomic cosine: SIX 0.596 · Digital Architects 0.538 · marketing control 0.492. Correct ranking — but the separation is THIN on short snippets. The conclusion is baked into the design: vectors are the recall filter; the cited LLM judge is mandatory for precision. (nomic task prefixes search_query:/search_document: were also measured: 0.585/0.567/0.486 — no meaningful gain on short texts; not worth the complexity now.)
  • The 0.5 recall floor is PROVISIONAL — it was ollwrite-measured on prose, not JDs; an atypical solo-founder profile may vector-match generic JDs poorly, so recalibrate after one week of real runs.

Two verification flags

  • The running POC container ignored/normalized the supplied document_id, and a reworded re-ingest produced a duplicate SIX row — the branch code supports stable-id upsert (routes.py:159), so the container is likely a stale Jul-8 image. Rebuild before relying on dedup.
  • The POC container runs the keyless hash embedder (EMBED_PROVIDER=local), so vector_score was empty — flip to EMBED_PROVIDER=ollama + OLLAMA_EMBED_BASE_URL=http://host.docker.internal:11434 for the real loop.

Decisions locked today

#DecisionLocked as
1Storeoll-memory pgvector, semantic-only lookup; location/date/status are metadata filters, never word-search
2EmbeddingsOllama nomic-embed-text, local, 768-dim (house rule: Ollama for retrieval, Groq for generation)
3Dedupsha256(canonical URL) as document_id (reposts change wording, not the link) — oll-memory upsert gives this for free
4JudgeOpenCLAW's inner Claude in the personal loop (subscription = zero marginal cost), using oll-scout's cited-verdict contract; oll-scout keeps llm_cited via oll-model for the paid multi-user arm
5Seam branchThe extension goes on feat/oll-mcp-thin (the thin seam already registered in Sam's openclaw.json); heavy PR #79 is superseded for this path
6Seam portHTTP port moves 5099 → 5097 (Postiz took 5099); update ~/.openclaw/openclaw.json to match
7SourcesOpen web + employer career pages; NO LinkedIn scraping; boards only via legal public surfaces (consistent with §5)

Milestones — earliest chargeable first

1  M1 — connect (~half a day)  ✅ EXECUTED 2026-07-11

+7 seam verbs · flip the POC memory container to ollama embeddings + rebuild (fixes the dedup flag) · re-register the seam on :5097 · one OpenCLAW cron + standing prompt → personal scout LIVE end-to-end on Telegram. Exit test: tomorrow 06:30 a real scored, cited alert arrives unprompted.

Operator conditions (Claw, 2026-07-11): (a) commit the interim memory/job-scout-zurich-aiops.md (8 postings) to the openclaw repo git BEFORE deleting it post-migration — a point-in-time snapshot in history, not a second live memory; (b) the flat CV file gets split into sections (summary / oll.am work / prior projects / stack / strengths) during migration, not assumed automatic.

Result — executed the same day it was specced:

  • oll-memory rebuilt: real nomic embeddings via local Ollama, stable document_id upsert verified — PLUS an additive caller-metadata-on-ingest change (feat/oll-memory@924b056) the state machine required; the running container had no metadata path at all.
  • Seam live on :5097: OpenCLAW registry fixed, 14 tools registered; the 7 job verbs shipped with 74 tests → PR #95 (awaiting Sam's merge gate; the tools run live locally from the branch regardless).
  • Operator (Claw) verified from inside the runtime and executed the migration: git snapshot → 8 postings stored → CV split into 5 sections → 3 standing queries → interim md deleted — then judged the backlog with the pinned rubric: 3 dismissed with cited reasons, 1 abstained on thin text (honest abstention working as designed).
  • Cron 49e0396e armed: daily 06:30 Europe/Zurich, isolated fresh session (forces the ToolSearch discipline). Exit test: the first unprompted cited Telegram alert, 2026-07-12 06:30.

WHAT WORKS — after the first burn-in weekend (2026-07-13)

Proven over the weekend  BUILT → TESTED → RUNNING LIVE since 2026-07-11

  • The full loop ran unattended on a 10-min cadence: open-web search (unrestricted sources + compounding source notes) → ollam_job_store (sha256(url) dedup, direct-employer-link policy with fetch verification) → the 0.64 recall floor (measured: all real postings ≥0.68, nonsense controls ≤0.60 — the old 0.5 passed even a pastry-chef control) → the inner-Claude judge with cited exhibits → Telegram alert tiers (≥85 instant / 70–84 digest / silence default) → the status state machine.
  • Corpus after the weekend: 25 jobs — 7 alerted, 5 watched, 12 dismissed, 1 expired; 17 of 25 carry the structured verdict contract (JSON: score/tier/sub_scores/exhibits with posting+CV quotes and cites/abstain_reason/ruling). 31 recorded cron runs.
  • The semantic store held up: oll-memory pgvector on real nomic embeddings, stable-id upsert dedup, status flips as metadata re-ingest; the CV lives as 6 bilingual sections of the real CV — the re-judge against it overturned 4 verdicts on day one.
  • The Dossier GUI (ollscout, localhost:5311, private repo): case sheets with typewritten exhibits, sub-score bars, verdict stamps, closest-miss callouts, honest abstention + a legacy fallback, RULE YOURSELF write-backs, j/k, /corpus.
  • Reliability as code: the seam under launchd (daily JWT re-mint), chromium+tini baked into the gateway image, local SearXNG (fixed a silent search-provider outage).

What broke over the weekend  FOUND + FIXED 2026-07-13 MORNING

  • The container's inner Claude CLI OAuth session expired Jul 12 ~12:06 and could not self-refresh — every tick after died in ~350ms (reason=session_expired). Fixed by re-lifting the host Keychain credentials into the container (the wiring playbook's Wall #2). LEARNING, tracked: containerized agent auth is a wasting asset — it needs either a periodic host-side credential re-sync or an alerting probe on cron status=error so a dead scout is noticed in minutes, not days. (The money-tier health watch pattern applies.)
  • Still open, unchanged: the thin-text quarantine, Claw's browser screenshot MCP bug, 8/25 legacy verdicts, coverage non-reproducible by design (an operator trade-off on record).

The operating model

Every plan was operator-reviewed before build; the operating agent caught five real bugs on day one (the fused-score display, the cron delivery-default spam, the floor drift, its own stale source note, the chromium detection layer) and respected a privilege gate rather than routing around it; the human side applied the privileged changes with verification, which caught a silent no-op. The weekend outage confirms the next reliability rung: the scout needs a watchdog, not more features.

Operator catches — three real bugs the operating agent found during review + execution

  • Fused score ≠ vector score: the seam displayed the rank-based RRF fused score instead of vector_score — it would have silently broken the 0.5 recall floor.
  • Silence-rule violation: the gateway cron auto-attached delivery:announce, which would have pushed raw agent output to Telegram daily even on zero-posting days — patched to explicit-alerts-only.
  • Floor recalibration is due sooner: the provisional 0.5 recall floor went 4-for-4 above on real postings — recalibrate after a few live runs, sooner than the planned one-week window.

NIGHT SHIFT — v2 executed (2026-07-13→14)

Sam ruled COMMIT — the night built it into the app  CONTRACT v5 · PR #95 · WITH THE OPERATOR

  • The ruling: Sam committed on the Dossier v2 design after the operator's truthfulness amendments and the measured ledger went in. The night executed that COMMIT into the running app, in collaboration with the operator.
  • Seam → CONTRACT v5, then +cv_read: 21 tools, 180 tests, on PR #95. New: ollam_tick_log/ollam_tick_register (rolling 8-tick register), source tri-state (pending → Sam approves via the GUI or ollam_source_rule), closest_miss first-class, an offer status, and ollam_cv_read (full-section evidence — see the defining bug below).
  • The app (ollscout e2ff7f0): every v2 element on REAL data with honest empty states — the tick band, THE LEDGER built from real closest_miss verdicts, lifecycle chips + advanceCase, corpus source approval, the ≥70 letterhead gate, rail filter/density, and draft VERSIONING (regenerate archives priors — the destructive-upsert bug class closed).
  • Operator's night: tick-logging + pending-source storage patched into the live cron; two more Rapidata www-twins reconciled (both re-judges caught real scoring errors); closest_miss emission confirmed in the cron contract.
  • Unattended lines held: no deploys, no merges — everything on commits + PR #95 for Sam's gates.

The night's defining bug — the judge was being starved of evidence, silently

The operator refused to close the AlpineAI re-judge: it could not find German fluency in the CV through any tool, so it would not rule. Told the fix was live, it refused AGAIN — it could not verify the new tool from its pre-reload session, and "accepting a plausible-sounding quote secondhand is no different from inventing one." That double refusal was correct both times, and it exposed the root cause: ollam_job_match clips evidence snippets at ~120 chars — the language line sat past the clip, so the judge kept ruling NO EVIDENCE on evidence that existed in the store. The fix is ollam_cv_read: read the full section before ruling NO EVIDENCE.

Outcome on given-content evidence: AlpineAI Senior SW Engineer 65 → 80, watched → ALERTED (Telegram sent) — a real match suppressed a full day by a rendering clip. LGT 60→63 on the same artifact; the other watched gaps were re-checked with full sections and honestly confirmed real.

2  M2 — converge + charge

Deploy oll-memory as the third spine (memory.oll.am) · merge PR #94 and point oll-scout's store at the SAME memory collections → deterministic sources + agent sources land in one corpus; oll-scout's plan-gate against oll-core (402) makes it the PAID product.

Known hardening from the M1 run: the seam JWT is 72h (expires 2026-07-14 — re-mint or wire the magic-link flow) · the seam runs as a nohup process (needs launchd for reboot survival) · recalibrate the 0.5 recall floor on live runs.

3  M3 — the front door

A web front door on the shared corpus — saved-searches UI, digest email via Core.

Ship-vs-build note

The fastest franc today is still deploying PR #94 + the live Stripe webhook (Sam console, ~3 min); this v2 layer is additive quality/coverage on the same product.

Open decisions for Sam

DecisionOptions / lean
Alert cadenceInstant-per-match vs 06:30 daily digest — recommend: instant for score ≥85, digest for the rest
Judge threshold to wake himRecommend: alert ≥70, watch-list 50–69, mirroring oll-scout's tiers
Heavy PR #79Close as superseded?

§Sources & grounding

This spec consolidates three sources into one: the v0 draft (research/SPEC_oll-scout_v0.md — CH source list, data model, acceptance test), the techniques teardown (why each technique is good, code-verified against the live OpenCLAW scout + gitroomhq/postiz-app), and the seam architecture (the scout as a driven pattern, not a new backend). The reuse claims — OpenCLAW loop, oll-model gateway, oll-memory retrieval, oll-mcp seam, Core auth + Stripe, Telegram — are all against live services.

Honesty line holds: matching-engine reductions are directional and model-dependent — validate on the real model, not the paper. No auto-apply, ever: no submit tool is registered. Legal sourcing rests on hiQ v. LinkedIn and schema.org/JobPosting; GDPR applies. No "Swiss-hosted" / data-residency claim.

Single next action

M1 — author the structured profile + wire the first JSON-LD source-watcher + the cited-rationale rubric on the proven loop. No submit tool, ever.