teft-leads 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- teft_leads-0.1.0/.claude/settings.local.json +18 -0
- teft_leads-0.1.0/.env.example +35 -0
- teft_leads-0.1.0/.gitignore +20 -0
- teft_leads-0.1.0/.python-version +1 -0
- teft_leads-0.1.0/CLAUDE.md +101 -0
- teft_leads-0.1.0/CONCEPT.md +132 -0
- teft_leads-0.1.0/LICENSE +5 -0
- teft_leads-0.1.0/NOTICE +14 -0
- teft_leads-0.1.0/PKG-INFO +78 -0
- teft_leads-0.1.0/README.md +54 -0
- teft_leads-0.1.0/SESSION_BOOTSTRAP.md +55 -0
- teft_leads-0.1.0/VALIDATE.md +64 -0
- teft_leads-0.1.0/docs/agents/domain.md +51 -0
- teft_leads-0.1.0/docs/agents/issue-tracker.md +45 -0
- teft_leads-0.1.0/docs/agents/triage-labels.md +15 -0
- teft_leads-0.1.0/docs/publishing.md +55 -0
- teft_leads-0.1.0/pyproject.toml +60 -0
- teft_leads-0.1.0/src/teft/__init__.py +9 -0
- teft_leads-0.1.0/src/teft/apify.py +230 -0
- teft_leads-0.1.0/src/teft/cli.py +157 -0
- teft_leads-0.1.0/src/teft/config.py +135 -0
- teft_leads-0.1.0/src/teft/enrich.py +249 -0
- teft_leads-0.1.0/src/teft/linkedin_urls.py +113 -0
- teft_leads-0.1.0/src/teft/models.py +40 -0
- teft_leads-0.1.0/src/teft/rank.py +256 -0
- teft_leads-0.1.0/src/teft/search.py +95 -0
- teft_leads-0.1.0/src/teft/serve/__init__.py +7 -0
- teft_leads-0.1.0/src/teft/serve/app.py +278 -0
- teft_leads-0.1.0/src/teft/serve/history.py +188 -0
- teft_leads-0.1.0/src/teft/serve/runs.py +402 -0
- teft_leads-0.1.0/src/teft/serve/settings.py +216 -0
- teft_leads-0.1.0/src/teft/serve/web.py +1143 -0
- teft_leads-0.1.0/src/teft/session.py +82 -0
- teft_leads-0.1.0/src/teft/source.py +63 -0
- teft_leads-0.1.0/src/teft/view.py +396 -0
- teft_leads-0.1.0/src/teft/wizard.py +172 -0
- teft_leads-0.1.0/tests/fixtures/humla_brief.txt +12 -0
- teft_leads-0.1.0/tests/test_apify.py +149 -0
- teft_leads-0.1.0/tests/test_config.py +139 -0
- teft_leads-0.1.0/tests/test_enrich.py +106 -0
- teft_leads-0.1.0/tests/test_linkedin_urls.py +67 -0
- teft_leads-0.1.0/tests/test_models.py +39 -0
- teft_leads-0.1.0/tests/test_rank.py +156 -0
- teft_leads-0.1.0/tests/test_serve.py +58 -0
- teft_leads-0.1.0/tests/test_serve_history.py +382 -0
- teft_leads-0.1.0/tests/test_serve_results.py +199 -0
- teft_leads-0.1.0/tests/test_serve_runs.py +611 -0
- teft_leads-0.1.0/tests/test_serve_settings.py +267 -0
- teft_leads-0.1.0/tests/test_view.py +76 -0
- teft_leads-0.1.0/tests/test_wizard.py +52 -0
- teft_leads-0.1.0/uv.lock +568 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Read(//Users/michaelwilhelmsen/.claude/commands/**)",
|
|
5
|
+
"Read(//Users/michaelwilhelmsen/.claude/skills/**)",
|
|
6
|
+
"Read(//Users/michaelwilhelmsen/.claude/**)",
|
|
7
|
+
"Read(//Users/michaelwilhelmsen/.claude/plugins/**)",
|
|
8
|
+
"Read(//Users/michaelwilhelmsen/.agents/skills/wayfinder/**)",
|
|
9
|
+
"Bash(gh issue *)",
|
|
10
|
+
"Bash(git ls-tree *)",
|
|
11
|
+
"Bash(git ls-remote *)",
|
|
12
|
+
"Bash(gh label *)",
|
|
13
|
+
"Bash(echo \"=== setup-matt-pocock-skills ===\" && cat ~/.claude/skills/setup-matt-pocock-skills/SKILL.md 2>/dev/null | head -60; echo \"=== search for a github tracker doc anywhere in ~/.claude/skills ===\" && grep -rl -i \"wayfinding operations\" ~/.claude/skills 2>/dev/null | head; grep -rl -i \"wayfinder:map\" ~/.claude 2>/dev/null | grep -iE 'tracker|github' | head)",
|
|
14
|
+
"Bash(gh api graphql -f 'query= *)",
|
|
15
|
+
"Bash(gh api *)"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copy to `.env` (git-ignored) and fill in. Never commit real values.
|
|
2
|
+
|
|
3
|
+
# --- LLM provider (the `rank` command / M3) ----------------------------------
|
|
4
|
+
# Default provider is Anthropic, model Sonnet 5 (cost-efficient; SKIP-rule calibration
|
|
5
|
+
# re-checked equivalent to Opus on the #4 fixtures). Set the key you use below.
|
|
6
|
+
ANTHROPIC_API_KEY=
|
|
7
|
+
OPENAI_API_KEY=
|
|
8
|
+
|
|
9
|
+
# Optional: switch the LLM. TEFT_LLM = anthropic (default) | openai.
|
|
10
|
+
# TEFT_MODEL overrides the per-provider default model id.
|
|
11
|
+
# anthropic default: claude-sonnet-5 (claude-opus-4-8 for the frontier tier)
|
|
12
|
+
# openai default: gpt-5.6-terra (gpt-5.6-luna cheaper, gpt-5.6-sol frontier)
|
|
13
|
+
# Re-run the fixtures before trusting a new provider — see VALIDATE.md.
|
|
14
|
+
# TEFT_LLM=openai
|
|
15
|
+
# TEFT_MODEL=gpt-5.6-terra
|
|
16
|
+
|
|
17
|
+
# --- Candidate source (the `scrape` step) ------------------------------------
|
|
18
|
+
# TEFT_SOURCE = browser (default) | apify
|
|
19
|
+
# browser: drives YOUR logged-in session (needs LI_AT below); $0, no external service.
|
|
20
|
+
# apify: a no-cookies Apify post-search actor scrapes on their infra — faster, and
|
|
21
|
+
# ban risk is off your account. Needs APIFY_TOKEN (apify.com free tier).
|
|
22
|
+
# TEFT_SOURCE=apify
|
|
23
|
+
# APIFY_TOKEN=
|
|
24
|
+
# TEFT_APIFY_ACTOR=harvestapi/linkedin-post-search # content-search (posts)
|
|
25
|
+
# TEFT_APIFY_PEOPLE_ACTOR=harvestapi/linkedin-profile-search # people-search (by title)
|
|
26
|
+
|
|
27
|
+
# --- Search tuning -----------------------------------------------------------
|
|
28
|
+
# How many candidates to pull per query before ranking (default 25).
|
|
29
|
+
# TEFT_MAX_CANDIDATES=25
|
|
30
|
+
|
|
31
|
+
# --- LinkedIn session (browser source only) ----------------------------------
|
|
32
|
+
# Your LinkedIn `li_at` session cookie — reused so teft drives YOUR logged-in
|
|
33
|
+
# session instead of automating a login. See SESSION_BOOTSTRAP.md for how to grab it.
|
|
34
|
+
# Optional: if the browser profile already holds a valid session you can leave this blank.
|
|
35
|
+
LI_AT=
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
|
|
11
|
+
# teft runtime data — NEVER commit session cookies or scraped people
|
|
12
|
+
.teft/
|
|
13
|
+
browser-profile/
|
|
14
|
+
candidates.json
|
|
15
|
+
shortlist-*.md
|
|
16
|
+
|
|
17
|
+
# secrets
|
|
18
|
+
.env
|
|
19
|
+
.env.*
|
|
20
|
+
!.env.example
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# teft — project guide
|
|
2
|
+
|
|
3
|
+
`teft` is a personal, local-first LinkedIn intent-signal lead-finder. It finds people posting
|
|
4
|
+
buyer-intent signals, runs each through a "why now" prompt (with a load-bearing SKIP rule), and
|
|
5
|
+
returns a ranked shortlist. No SaaS, no hosting. Candidate source is pluggable (`TEFT_SOURCE`):
|
|
6
|
+
`browser` (default — drives Michael's *own* logged-in session via Patchright + `li_at` reuse, $0,
|
|
7
|
+
no external service) or `apify` (a no-cookies Apify post-search actor scrapes on their infra —
|
|
8
|
+
faster, ban risk off the account, opt-in external service). Planning lives in GitHub Issues (map
|
|
9
|
+
#1, build plan #6, impl map #7).
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv run teft run # guided: config → product/brief/phrases → scrape → rank → view
|
|
15
|
+
uv run teft view # local results dashboard (reads latest ~/.teft/shortlist-*.json)
|
|
16
|
+
uv run teft serve # FastAPI web app: BYOK settings, search/re-rank, results, history
|
|
17
|
+
uv run teft scrape -p "phrase" [-p …] [-t "job title" …] [--discover-only] [--dump] [--emails]
|
|
18
|
+
# -p → content-search (signal leads); -t → people-search (role-fit leads, apify only)
|
|
19
|
+
uv run teft rank --brief brief.txt # candidates.json → why-now → shortlist-<date>.{md,json}
|
|
20
|
+
uv run teft bootstrap # verify the logged-in LinkedIn session
|
|
21
|
+
uv run teft version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Pipeline: `scrape` (content-search → per-candidate enrich → `~/.teft/candidates.json`) → `rank`
|
|
25
|
+
(→ `shortlist-<date>.{md,json}`) → `view`. `teft run` chains all three. The scrape/rank split is
|
|
26
|
+
deliberate: scrape once, re-run/tune `rank` for free.
|
|
27
|
+
|
|
28
|
+
## Config (env, auto-loaded from `.env` — no manual `source`)
|
|
29
|
+
|
|
30
|
+
- `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` — key for the chosen provider (`rank`).
|
|
31
|
+
- `TEFT_SOURCE` — `browser` (default) | `apify`. `apify` needs `APIFY_TOKEN`; `browser` needs `LI_AT`.
|
|
32
|
+
- `APIFY_TOKEN` — Apify API token (free tier) for `TEFT_SOURCE=apify`.
|
|
33
|
+
- `TEFT_APIFY_ACTOR` — post-search actor id (default `harvestapi/linkedin-post-search`).
|
|
34
|
+
- `TEFT_APIFY_PEOPLE_ACTOR` — people-search actor id (default `harvestapi/linkedin-profile-search`).
|
|
35
|
+
- `LI_AT` — LinkedIn session cookie for `browser` source (or sign in once in the persistent profile).
|
|
36
|
+
- `TEFT_LLM` — `anthropic` (default, `claude-sonnet-5`) | `openai` (`gpt-5.6-terra`).
|
|
37
|
+
- `TEFT_MODEL` — override the model id (e.g. `claude-opus-4-8`, `gpt-5.6-sol`, `gpt-5.6-luna`).
|
|
38
|
+
- `TEFT_HEADLESS=1` — invisible/faster browser (default headful is stealthier).
|
|
39
|
+
- `TEFT_MIN_DELAY` / `TEFT_MAX_DELAY` — pacing between candidates (speed vs ban-safety).
|
|
40
|
+
- `TEFT_MAX_CANDIDATES` — int cap per query (default 25 → `Config.max_candidates_per_query`).
|
|
41
|
+
- `TEFT_DATA_DIR` — data dir (default `~/.teft`).
|
|
42
|
+
|
|
43
|
+
`teft run` prompts for and persists any missing key/cookie to `.env` (git-ignored). Data dir holds
|
|
44
|
+
`candidates.json`, `shortlist-*.{md,json}`, `actions.json` (contacted+notes), `campaign.json`,
|
|
45
|
+
`browser-profile/`.
|
|
46
|
+
|
|
47
|
+
## Dev
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv sync
|
|
51
|
+
uv run pyright # type check (must be clean)
|
|
52
|
+
uv run pytest -q # tests
|
|
53
|
+
uv run ruff check . # lint
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Stack: Python 3.12, `uv`, `typer`, `pydantic`, `patchright`, `anthropic`, `openai`, `httpx`. `src/teft/`:
|
|
57
|
+
`config` · `models` · `session` (Patchright) · `search` (M1) · `enrich` (M2) · `apify` (post- +
|
|
58
|
+
profile-search sources) · `source` (browser/apify × content/people dispatch) · `rank` (M3, provider
|
|
59
|
+
seam; **two prompts** — signal why-now for content-search, softer role-fit for people-search, keyed
|
|
60
|
+
on `Candidate.source`) · `view` (stdlib server + dashboard) · `serve/` (FastAPI web app: `app`
|
|
61
|
+
routes · `web` inline UI · `runs` engine · `settings` BYOK · `history` records/compare) · `wizard`
|
|
62
|
+
(`teft run`) · `cli`.
|
|
63
|
+
|
|
64
|
+
## Gotchas / non-obvious facts
|
|
65
|
+
|
|
66
|
+
- **LinkedIn DOM (verified live):** profile pages have **no `<h1>`/og: tags** — name comes from the
|
|
67
|
+
page `<title>` (`name_from_title` handles both profile and activity title shapes, language-agnostic).
|
|
68
|
+
Activity page is stable: post = `.update-components-text`, headline =
|
|
69
|
+
`.update-components-actor__description` (NOT `__title`, which is the name), date =
|
|
70
|
+
`.update-components-actor__sub-description`, post urn from `[data-urn^="urn:li:activity"]`.
|
|
71
|
+
- **Enrichment is one nav/candidate** (activity page only); email is opt-in (`--emails`, extra nav,
|
|
72
|
+
usually empty for cold leads).
|
|
73
|
+
- **Never use `networkidle` waits** — LinkedIn never idles; wait for the specific selector instead.
|
|
74
|
+
- **SKIP-rule calibration** validated on Opus (#4); re-checked equivalent on **Sonnet 5** (now the
|
|
75
|
+
default) via `tests/fixtures/` on 2026-07-16. Re-run the fixtures before trusting any new provider/
|
|
76
|
+
tier. Prompt is ported verbatim from the `prototype/why-now-prompt` branch.
|
|
77
|
+
- Keep Patchright (stealth, #2) for the `browser` source — don't swap to plain Playwright.
|
|
78
|
+
- **Apify source is defensive/unvalidated:** `candidate_from_post_item` probes many field-name
|
|
79
|
+
variants because actor output schemas differ; the exact mapping + the actor input (`keywords`,
|
|
80
|
+
`maxItems`) are unverified against a live token — tune from a `--dump` (`~/.teft/dump/apify-*.json`).
|
|
81
|
+
- Avatars are initials (LinkedIn photo URLs are session-gated, don't render externally).
|
|
82
|
+
- `teft view` is localhost-only, single-user, no auth — don't expose the port.
|
|
83
|
+
- Pure logic (URL/email/name/title parsing, prompt plumbing, actions) is unit-tested; DOM selectors
|
|
84
|
+
are the thin live layer, tunable from `--dump` captures.
|
|
85
|
+
|
|
86
|
+
## Agent skills
|
|
87
|
+
|
|
88
|
+
### Issue tracker
|
|
89
|
+
|
|
90
|
+
GitHub Issues via the `gh` CLI (repo `michaelwilhelmsen/linkedin-lead-finder`); this is also where the
|
|
91
|
+
wayfinder maps live (#1, #6, #7). See `docs/agents/issue-tracker.md`.
|
|
92
|
+
|
|
93
|
+
### Triage labels
|
|
94
|
+
|
|
95
|
+
The five canonical roles, label strings unchanged (`needs-triage`, `needs-info`, `ready-for-agent`,
|
|
96
|
+
`ready-for-human`, `wontfix`) — distinct from the `wayfinder:*` labels. See `docs/agents/triage-labels.md`.
|
|
97
|
+
|
|
98
|
+
### Domain docs
|
|
99
|
+
|
|
100
|
+
Single-context: `CONTEXT.md` + `docs/adr/` at the repo root (created lazily by `/domain-modeling`, not
|
|
101
|
+
scaffolded upfront). See `docs/agents/domain.md`.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# LinkedIn Lead Finder — Concept Seed
|
|
2
|
+
|
|
3
|
+
> **This document is the seed input for a follow-up wayfinder session in this directory.** It captures the reference product, why we're building our own instead of using theirs, sample output shape, and the open questions wayfinder should help answer. Don't treat this as a spec — treat it as the raw brief.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The core idea (one paragraph)
|
|
8
|
+
|
|
9
|
+
Build a tool that takes a description of a product (what it does, who it's for, the pain it solves) and returns a ranked list of high-intent LinkedIn prospects — not scraped-database contacts, but people whose *recent posts, role changes, and public activity* signal they are actively in-market. Each prospect comes with a fit score and a plain-English "why this person, right now" explanation that a human could actually use as the opening line of an outreach message. The angle is "real signals, not stored databases" — surface intent from live public activity rather than filtering a static contact list on firmographics.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Reference: what SeedOps.co does
|
|
14
|
+
|
|
15
|
+
SeedOps (seedops.co) is the tool the Reddit DM was pitching. It's an early-stage AI lead-gen product with a clear positioning hook that's worth stealing conceptually even if we don't copy the implementation.
|
|
16
|
+
|
|
17
|
+
**Pitch:** "Find and outreach high-intent leads with AI. Real signals, not stored databases."
|
|
18
|
+
|
|
19
|
+
**How it works (from their homepage):**
|
|
20
|
+
- User creates a "campaign" and describes their ideal lead in natural language (e.g. "decision-makers actively frustrated with their current vendor and looking to buy a replacement in the last 30 days")
|
|
21
|
+
- SeedOps runs the campaign as a persistent "signal" — it monitors public activity 24/7 and surfaces matches
|
|
22
|
+
- Each match gets a fit score (0–100%) and a short LLM-generated explanation of *why* this person fits *this* campaign *right now*
|
|
23
|
+
- Results are shown in a table: `# | Prospect | Signal | Signal Date | Fit | Found Date`
|
|
24
|
+
- Export to CSV, then plug into any outreach tool (they don't do automated outreach themselves — their words: "for this early stage, no. The only option is to export them and use any outreach tool")
|
|
25
|
+
|
|
26
|
+
**Signal types they surface** (from their landing page examples):
|
|
27
|
+
- **Buying signal** — "Our lead research is still painfully manual. Looking for a tool that surfaces genuinely high-intent prospects..."
|
|
28
|
+
- **Competitor signal** — "Finally switched off our old CRM. The reporting was a nightmare..."
|
|
29
|
+
- **Hiring signal** — "Scaling the team fast this quarter — six open roles..."
|
|
30
|
+
- **Fundraising signal** — "Thrilled to share we just closed our seed round..."
|
|
31
|
+
- **Investment signal** — "We are already backing some incredible companies... Seed-stage founders, my DMs are open."
|
|
32
|
+
- **Expansion signal** — "We're opening our first office in Dubai..."
|
|
33
|
+
|
|
34
|
+
**Pricing (for reference):**
|
|
35
|
+
- Free: 500 credits one-time, 1 running signal, 30 rows/table
|
|
36
|
+
- Pro: $89/mo, 6,000 credits, 5 running signals, unlimited rows
|
|
37
|
+
- Enterprise: custom
|
|
38
|
+
|
|
39
|
+
**Positioning vs. incumbents** (from their compare table):
|
|
40
|
+
- Apollo & lemlist = stored database + cold leads
|
|
41
|
+
- SeedOps = real-time data + real intent signals + AI-powered leads
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Sample output (from the Reddit DM screenshots)
|
|
46
|
+
|
|
47
|
+
The redditor shared a live campaign called **"Meeting Management for Agencies an..."** (truncated in the screenshot) with the subtitle *"Creative and sales agencies looking for meeting workflow tools"* — 35 results, targeting a product called **Humla** (a meeting/notes/workflow product). This is the concrete output shape wayfinder should aim to match or improve on:
|
|
48
|
+
|
|
49
|
+
**Table columns:** `# | Prospect (name + LinkedIn headline) | Signal (link to source post) | Signal Date | Fit % | Found Date`
|
|
50
|
+
|
|
51
|
+
**Actual "Fit" explanations from the screenshots** (verbatim, so wayfinder has a concrete target for the LLM output quality):
|
|
52
|
+
|
|
53
|
+
> **Antony Puttick — Group Creative Director | Drummer | Relentlessly Curious — 85% fit — Jul 14, 2026**
|
|
54
|
+
> "Antony is ideal for Humla because he has spent a year building an automated micro-agency workflow specifically triggered by meeting notes, and this means he is currently in the active stage of optimizing the bridge between client recordings and collaborative project management."
|
|
55
|
+
|
|
56
|
+
> **Miladys Cruz-Fisher — Principal & Creative AI Strategist — 85% fit — Jul 2, 2026**
|
|
57
|
+
> "Miladys is ideal for Humla because she has officially relaunched her agency as an AI strategy firm, and this means she is currently building her new internal stack and client service workflows while facing immediate pressure to implement the very AI tools she advocates for."
|
|
58
|
+
|
|
59
|
+
> **Kyle — 85% fit — Jun 19, 2026**
|
|
60
|
+
> "Kyle is ideal for Humla because he is currently expanding his sales team at an AI company, and this means he has a fresh mandate to establish scalable meeting workflows and ensure client records are collaborative as new hires onboard."
|
|
61
|
+
|
|
62
|
+
> **Gillian — 82% fit — Jul 9, 2026**
|
|
63
|
+
> "Gillian is ideal for Humla because she explicitly calls out a capturing problem where valuable insights are lost in meeting notes and siloed updates, and this means her team lacks the collaborative workflow tools needed to surface and centralize client stories."
|
|
64
|
+
|
|
65
|
+
> **Ian — 82% fit — Jul 14, 2026**
|
|
66
|
+
> "Ian is ideal for Humla because he just transitioned to a Director leadership role focused on coaching and empowering a larger [team]... he now faces a fresh mandate to implement scalable [workflows]..."
|
|
67
|
+
|
|
68
|
+
**What's notable about the output quality:**
|
|
69
|
+
- Each explanation follows a `[Person] is ideal for [Product] because [observed fact], and this means [inferred current need]` structure — the "and this means" bridge is the money shot; it's what turns a post into a buying signal.
|
|
70
|
+
- Fit is expressed as a % with a colored dot, not a category. Feels precise even though it's clearly LLM-scored.
|
|
71
|
+
- Signal date and Found date are separate columns — implying the system stores *when* the intent post happened vs. *when* the tool surfaced it.
|
|
72
|
+
- All fits in the shown sample sit in the 82–85% band. Either the threshold is high or the calibration is loose — worth investigating.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Why build our own instead of using SeedOps
|
|
77
|
+
|
|
78
|
+
This is a three-birds-one-stone move for Michael, not a straight "cheaper clone" play:
|
|
79
|
+
|
|
80
|
+
1. **Own the tool at Screenpartner.** The agency is chronically fighting for larger projects and haggled-down estimates. A working intent-signal engine plugged into Screenpartner's own outbound (marketing agencies, WordPress-shaped businesses, integrations pain-points) could directly move the needle on landing 1M+ NOK projects. That's the "1-2M NOK annual surplus in 5 years" company goal.
|
|
81
|
+
|
|
82
|
+
2. **Learn the mechanics.** LLM-driven scoring + signal extraction from unstructured web/LinkedIn data is a stack Michael's weakest in (Python, data pipelines, DevOps). Building this is a legitimate skills investment, not a distraction. AI has "removed the barrier of entry for unfamiliar frameworks" per Michael's own notes.
|
|
83
|
+
|
|
84
|
+
3. **Potential side-product / recurring revenue.** Fits Michael's stated dream (passive/recurring revenue alongside agency salary). Killer hook is already validated by the reddit DM: the pitch worked well enough on Michael that he wants to build it. The seedops founder even admits their own ICP is "so wide" — meaning there's room for a *narrower*, better-positioned tool (e.g. "for agencies selling into agencies", "for WordPress consultancies", etc.).
|
|
85
|
+
|
|
86
|
+
The trap to avoid: this becomes yet another abandoned side project (woodworking app, quotes app, git-timetrack). Wayfinder should push hard on scope. A weekend-shippable v0 that Michael actually uses for Screenpartner outbound is worth 10× a beautiful spec that never gets built.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Open questions (this is the wayfinder agenda)
|
|
91
|
+
|
|
92
|
+
**Product shape**
|
|
93
|
+
- Is this a personal tool for Screenpartner first, or a SaaS from day one? (The answer changes everything about auth, billing, multi-tenancy.)
|
|
94
|
+
- Lead list only, or automated outreach too? SeedOps punted on outreach — is that a real gap or a smart focus?
|
|
95
|
+
- LinkedIn-only or multi-source (Twitter/X, Reddit, HN, company blogs, hiring pages, press releases)?
|
|
96
|
+
|
|
97
|
+
**Input shape**
|
|
98
|
+
- Freeform natural-language campaign brief (SeedOps' approach) vs. structured form (product name, ICP, pain points, competitor list)? A hybrid?
|
|
99
|
+
- Does the user upload their own product one-pager / website URL and we extract positioning automatically?
|
|
100
|
+
|
|
101
|
+
**Data sources & the LinkedIn problem**
|
|
102
|
+
- LinkedIn actively blocks scraping. What's the legal/practical path? Options include: (a) LinkedIn Sales Navigator API + partner status (hard), (b) third-party providers who resell LinkedIn data (Apollo, Proxycurl, Nubela, RocketReach — but these are "stored database"), (c) user-brings-their-own-LinkedIn-session via a browser extension (grey area but scrappy), (d) non-LinkedIn signals only (Twitter, GitHub, personal blogs, company news).
|
|
103
|
+
- What does "real-time" actually mean in practice? Polling frequency vs. cost.
|
|
104
|
+
|
|
105
|
+
**Scoring & LLM prompt design**
|
|
106
|
+
- The "and this means…" template in SeedOps' output is a specific prompt engineering pattern. Reverse-engineering that prompt is probably 30 minutes of work and gets us 70% of the output quality.
|
|
107
|
+
- How is Fit % computed? LLM self-rating (unreliable), embedding similarity to ICP description (calibratable), or a hybrid?
|
|
108
|
+
|
|
109
|
+
**Enrichment**
|
|
110
|
+
- How much per-lead enrichment do we do (email finding, company data, mutual connections)? Each layer adds cost.
|
|
111
|
+
|
|
112
|
+
**Tech stack**
|
|
113
|
+
- Michael's strengths: WordPress/PHP/ACF, JS, CSS. Intermediate: Python, Next.js, React. Weakest: DevOps.
|
|
114
|
+
- Realistic stack candidates: Next.js + Vercel + Supabase + OpenAI/Anthropic API + a scraping/signal-fetch layer (Apify, Bright Data, or custom). Or, for a "start ugly and ship" v0: a Python script + Postgres + a plain HTML dashboard.
|
|
115
|
+
- Cron/queue for signal-polling: Vercel Cron? Trigger.dev? A tiny VPS with systemd?
|
|
116
|
+
|
|
117
|
+
**Cost model (self-imposed constraint)**
|
|
118
|
+
- What's the per-lead cost ceiling before the economics don't work? SeedOps sells 6,000 credits for $89 — that's ~1.5 cents per credit. If a "credit" = one lead surfaced, that's very tight margin unless data-source costs are negligible.
|
|
119
|
+
|
|
120
|
+
**Time budget (self-imposed constraint)**
|
|
121
|
+
- Michael has 1–2 hours on weekday evenings, 8–11 PM. First useful version should exist within 2–3 weeks of evenings, not months. What's the smallest slice that produces one usable Screenpartner lead?
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## What wayfinder should NOT do in the next session
|
|
126
|
+
|
|
127
|
+
- Don't spec out architecture in detail.
|
|
128
|
+
- Don't write code.
|
|
129
|
+
- Don't pick a name (`linkedin-lead-finder` is a placeholder directory name — the name comes later).
|
|
130
|
+
- Don't attempt to solve the LinkedIn scraping question definitively; just enumerate the realistic paths.
|
|
131
|
+
|
|
132
|
+
The next session's job is: **narrow the shape.** Personal tool vs. SaaS. LinkedIn-only vs. multi-source. Freeform vs. structured input. Lead list vs. outreach. Get to a one-paragraph "here's the v0 we're building and here's what it does" — then the session after that can plan the build.
|
teft_leads-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2026 Michael Wilhelmsen / Screenpartner. All rights reserved.
|
|
2
|
+
|
|
3
|
+
No permission is granted to any person to use, copy, modify, or distribute this
|
|
4
|
+
software, in whole or in part, in source or binary form, without the prior
|
|
5
|
+
written consent of the copyright holder.
|
teft_leads-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
teft
|
|
2
|
+
Copyright 2026 Michael Wilhelmsen
|
|
3
|
+
|
|
4
|
+
This product includes software adapted from:
|
|
5
|
+
|
|
6
|
+
linkedin-mcp-server
|
|
7
|
+
https://github.com/stickerdaniel/linkedin-mcp-server
|
|
8
|
+
Copyright (c) Daniel Sticker and contributors
|
|
9
|
+
Licensed under the Apache License, Version 2.0.
|
|
10
|
+
|
|
11
|
+
Specifically, teft's LinkedIn URL normalization and person-link classification
|
|
12
|
+
(src/teft/linkedin_urls.py) are adapted from that project's
|
|
13
|
+
`linkedin_mcp_server/scraping/link_metadata.py`. A copy of the Apache-2.0
|
|
14
|
+
license is available at http://www.apache.org/licenses/LICENSE-2.0.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: teft-leads
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first LinkedIn intent-signal lead-finder with a local web app — real signals, not stored databases.
|
|
5
|
+
Project-URL: Repository, https://github.com/michaelwilhelmsen/linkedin-lead-finder
|
|
6
|
+
Author-email: Michael Wilhelmsen <michael@screenpartner.no>
|
|
7
|
+
License-Expression: LicenseRef-Proprietary
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: <3.13,>=3.12
|
|
15
|
+
Requires-Dist: anthropic>=0.40
|
|
16
|
+
Requires-Dist: fastapi>=0.115
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: openai>=2.0
|
|
19
|
+
Requires-Dist: patchright>=1.40
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
22
|
+
Requires-Dist: uvicorn>=0.30
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# teft
|
|
26
|
+
|
|
27
|
+
> **teft** *(Norwegian: the instinctive nose for a lead — a scent for who's in-market right now).*
|
|
28
|
+
> Repo slug is still `linkedin-lead-finder`; the tool is named `teft`.
|
|
29
|
+
|
|
30
|
+
A personal, local-first tool that turns a freeform brief into a ranked, explained
|
|
31
|
+
shortlist of high-intent LinkedIn prospects — "real signals, not stored databases."
|
|
32
|
+
|
|
33
|
+
## For colleagues — try it in 2 minutes
|
|
34
|
+
|
|
35
|
+
Nothing to clone. It runs entirely on your own machine (the web app is `127.0.0.1`-only).
|
|
36
|
+
|
|
37
|
+
1. **Install uv:** `curl -LsSf https://astral.sh/uv/install.sh | sh` (Windows: see [docs.astral.sh/uv](https://docs.astral.sh/uv))
|
|
38
|
+
2. **Run:** `uvx teft-leads serve` — your browser opens the app. (PyPI name is `teft-leads`; the command you type once installed is still `teft`.)
|
|
39
|
+
3. **In the app:** Settings → paste your keys → Search → run it.
|
|
40
|
+
|
|
41
|
+
That's it. The rest of this page is for people building on `teft`.
|
|
42
|
+
|
|
43
|
+
## About / planning
|
|
44
|
+
|
|
45
|
+
Planned with the **wayfinder** workflow; the plan lives as **GitHub Issues**. Two maps:
|
|
46
|
+
|
|
47
|
+
- [**Planning map (#1)**](../../issues/1) — *complete.* Destination reached: the locked v0 definition.
|
|
48
|
+
- [**Build plan (#6)**](../../issues/6) — stack, LLM, architecture, name, milestones.
|
|
49
|
+
- [**Implementation map (#7)**](../../issues/7) — **active.** v0 as five `wayfinder:task` milestones (M0→M4);
|
|
50
|
+
the **frontier** (open, unblocked, unassigned) is the work takeable now — currently
|
|
51
|
+
[**M0 (#8)**](../../issues/8).
|
|
52
|
+
|
|
53
|
+
See [`CONCEPT.md`](CONCEPT.md) for the original seed brief.
|
|
54
|
+
|
|
55
|
+
## v0 at a glance
|
|
56
|
+
|
|
57
|
+
Python CLI on your Mac, over your *own* logged-in LinkedIn session (Patchright, `li_at` cookie reuse).
|
|
58
|
+
|
|
59
|
+
**Guided:** `teft run` — one command walks you through config (prompts + saves any missing key/cookie),
|
|
60
|
+
then *product → brief → buyer-intent phrases* (or reuse the last campaign), scrapes, ranks, and opens the
|
|
61
|
+
results UI. Defaults to headless/fast.
|
|
62
|
+
|
|
63
|
+
**Or step by step:**
|
|
64
|
+
- **`teft scrape -p "phrase" …`** — content-search → per-candidate activity/profile → `candidates.json`.
|
|
65
|
+
- **`teft rank --brief brief.txt`** — each candidate → the "why now" prompt, SKIP filter → ranked
|
|
66
|
+
`shortlist-<date>.{md,json}`.
|
|
67
|
+
- **`teft view`** — a local, zero-dependency results UI: lead cards, expandable post, Open profile / Open
|
|
68
|
+
post / copy email, and contacted + notes saved to `~/.teft/actions.json`.
|
|
69
|
+
|
|
70
|
+
No Fit %, no monitoring, no outreach, no external services. `.env` is auto-loaded (no manual `source`).
|
|
71
|
+
Speed/stealth knobs: `TEFT_HEADLESS=1`, `TEFT_MIN_DELAY` / `TEFT_MAX_DELAY`, `TEFT_MAX_CANDIDATES` (cap per query, default 25).
|
|
72
|
+
|
|
73
|
+
## LLM provider
|
|
74
|
+
|
|
75
|
+
`rank` is provider-pluggable. Default is `claude-opus-4-8` (the tier the "why now" prompt's SKIP-rule
|
|
76
|
+
calibration was validated on). To use GPT-5.6 instead, set `TEFT_LLM=openai` (defaults to `gpt-5.6-terra`;
|
|
77
|
+
override with `TEFT_MODEL`, e.g. `gpt-5.6-sol` / `gpt-5.6-luna`) and `OPENAI_API_KEY`. **The SKIP calibration
|
|
78
|
+
is only proven on Claude — re-run the fixtures on a new provider before trusting it** (see [VALIDATE.md](VALIDATE.md)).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# teft
|
|
2
|
+
|
|
3
|
+
> **teft** *(Norwegian: the instinctive nose for a lead — a scent for who's in-market right now).*
|
|
4
|
+
> Repo slug is still `linkedin-lead-finder`; the tool is named `teft`.
|
|
5
|
+
|
|
6
|
+
A personal, local-first tool that turns a freeform brief into a ranked, explained
|
|
7
|
+
shortlist of high-intent LinkedIn prospects — "real signals, not stored databases."
|
|
8
|
+
|
|
9
|
+
## For colleagues — try it in 2 minutes
|
|
10
|
+
|
|
11
|
+
Nothing to clone. It runs entirely on your own machine (the web app is `127.0.0.1`-only).
|
|
12
|
+
|
|
13
|
+
1. **Install uv:** `curl -LsSf https://astral.sh/uv/install.sh | sh` (Windows: see [docs.astral.sh/uv](https://docs.astral.sh/uv))
|
|
14
|
+
2. **Run:** `uvx teft-leads serve` — your browser opens the app. (PyPI name is `teft-leads`; the command you type once installed is still `teft`.)
|
|
15
|
+
3. **In the app:** Settings → paste your keys → Search → run it.
|
|
16
|
+
|
|
17
|
+
That's it. The rest of this page is for people building on `teft`.
|
|
18
|
+
|
|
19
|
+
## About / planning
|
|
20
|
+
|
|
21
|
+
Planned with the **wayfinder** workflow; the plan lives as **GitHub Issues**. Two maps:
|
|
22
|
+
|
|
23
|
+
- [**Planning map (#1)**](../../issues/1) — *complete.* Destination reached: the locked v0 definition.
|
|
24
|
+
- [**Build plan (#6)**](../../issues/6) — stack, LLM, architecture, name, milestones.
|
|
25
|
+
- [**Implementation map (#7)**](../../issues/7) — **active.** v0 as five `wayfinder:task` milestones (M0→M4);
|
|
26
|
+
the **frontier** (open, unblocked, unassigned) is the work takeable now — currently
|
|
27
|
+
[**M0 (#8)**](../../issues/8).
|
|
28
|
+
|
|
29
|
+
See [`CONCEPT.md`](CONCEPT.md) for the original seed brief.
|
|
30
|
+
|
|
31
|
+
## v0 at a glance
|
|
32
|
+
|
|
33
|
+
Python CLI on your Mac, over your *own* logged-in LinkedIn session (Patchright, `li_at` cookie reuse).
|
|
34
|
+
|
|
35
|
+
**Guided:** `teft run` — one command walks you through config (prompts + saves any missing key/cookie),
|
|
36
|
+
then *product → brief → buyer-intent phrases* (or reuse the last campaign), scrapes, ranks, and opens the
|
|
37
|
+
results UI. Defaults to headless/fast.
|
|
38
|
+
|
|
39
|
+
**Or step by step:**
|
|
40
|
+
- **`teft scrape -p "phrase" …`** — content-search → per-candidate activity/profile → `candidates.json`.
|
|
41
|
+
- **`teft rank --brief brief.txt`** — each candidate → the "why now" prompt, SKIP filter → ranked
|
|
42
|
+
`shortlist-<date>.{md,json}`.
|
|
43
|
+
- **`teft view`** — a local, zero-dependency results UI: lead cards, expandable post, Open profile / Open
|
|
44
|
+
post / copy email, and contacted + notes saved to `~/.teft/actions.json`.
|
|
45
|
+
|
|
46
|
+
No Fit %, no monitoring, no outreach, no external services. `.env` is auto-loaded (no manual `source`).
|
|
47
|
+
Speed/stealth knobs: `TEFT_HEADLESS=1`, `TEFT_MIN_DELAY` / `TEFT_MAX_DELAY`, `TEFT_MAX_CANDIDATES` (cap per query, default 25).
|
|
48
|
+
|
|
49
|
+
## LLM provider
|
|
50
|
+
|
|
51
|
+
`rank` is provider-pluggable. Default is `claude-opus-4-8` (the tier the "why now" prompt's SKIP-rule
|
|
52
|
+
calibration was validated on). To use GPT-5.6 instead, set `TEFT_LLM=openai` (defaults to `gpt-5.6-terra`;
|
|
53
|
+
override with `TEFT_MODEL`, e.g. `gpt-5.6-sol` / `gpt-5.6-luna`) and `OPENAI_API_KEY`. **The SKIP calibration
|
|
54
|
+
is only proven on Claude — re-run the fixtures on a new provider before trusting it** (see [VALIDATE.md](VALIDATE.md)).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# M0 — session bootstrap + content-search go/no-go (you run this)
|
|
2
|
+
|
|
3
|
+
The scaffold is built. These last two steps touch your **personal LinkedIn account**
|
|
4
|
+
(cookie + live queries, with real ban risk), so they're yours to run — not automated.
|
|
5
|
+
This is the go/no-go for the whole v0 slice.
|
|
6
|
+
|
|
7
|
+
## 0. Install (one time)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync # installs deps into .venv
|
|
11
|
+
uv run patchright install chrome # downloads the browser Patchright drives
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 1. Grab your `li_at` cookie
|
|
15
|
+
|
|
16
|
+
1. Log in to LinkedIn in your normal browser.
|
|
17
|
+
2. DevTools → Application → Cookies → `https://www.linkedin.com` → copy the **`li_at`** value.
|
|
18
|
+
3. `cp .env.example .env`, paste it as `LI_AT=...`, add your `ANTHROPIC_API_KEY` (for later, M3).
|
|
19
|
+
|
|
20
|
+
`.env` is git-ignored — never commit it. Treat `li_at` like a password.
|
|
21
|
+
|
|
22
|
+
## 2. Verify the session
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
set -a && source .env && set +a
|
|
26
|
+
uv run teft bootstrap
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Expect `✓ Authenticated — your session works.` A headful Chrome opens as you. If it
|
|
30
|
+
fails, sign in once inside the window; the persistent profile keeps the session so you
|
|
31
|
+
can leave `LI_AT` blank afterward.
|
|
32
|
+
|
|
33
|
+
## 3. The content-search go/no-go (the load-bearing risk)
|
|
34
|
+
|
|
35
|
+
Manually — before we build M1 — confirm LinkedIn **content search** returns usable
|
|
36
|
+
recent posts from strangers via your session. In the Chrome that `bootstrap` opened, or
|
|
37
|
+
by hand:
|
|
38
|
+
|
|
39
|
+
1. Visit `https://www.linkedin.com/search/results/content/?keywords=<a Humla buyer-intent phrase>`
|
|
40
|
+
(e.g. *"lost in meeting notes"*, *"client follow-up"*, *"onboarding new AEs"*).
|
|
41
|
+
2. Eyeball ~10–20 results. For each, can you see: **author**, **post text**, **date**, and
|
|
42
|
+
click through to a profile?
|
|
43
|
+
3. Try 2–3 phrases. Note roughly how many results feel like real intent vs. noise, and
|
|
44
|
+
whether you hit a search-limit wall.
|
|
45
|
+
|
|
46
|
+
## 4. Record the decision on the ticket
|
|
47
|
+
|
|
48
|
+
Comment on **[M0 (#8)](../../issues/8)** with one of:
|
|
49
|
+
|
|
50
|
+
- **GO** — content-search returns usable posts. → unblocks M1 (#9); we build `content_search`.
|
|
51
|
+
- **NO-GO** — too gated/thin. → we re-scope the smallest slice to **filtered people-search**
|
|
52
|
+
and update the map (#7) before building further. (This is a fine outcome — it's exactly
|
|
53
|
+
what M0 exists to find out cheaply.)
|
|
54
|
+
|
|
55
|
+
Then close #8. M1 is blocked-by #8, so it stays off the frontier until you do.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Afternoon validation — the batched live session
|
|
2
|
+
|
|
3
|
+
All four milestones' **code** is on `main`. The offline half (M3 rank) is tested and
|
|
4
|
+
its prompt is proven (#4). What's left is the **live half** — validating the two
|
|
5
|
+
LinkedIn-driving steps against your own session — plus one end-to-end judgment.
|
|
6
|
+
Do these in order; each unblocks the next. One session, ~30–45 min.
|
|
7
|
+
|
|
8
|
+
Prereq: `uv sync && uv run patchright install chrome`, and `.env` filled
|
|
9
|
+
(`LI_AT`, `ANTHROPIC_API_KEY`) — see [SESSION_BOOTSTRAP.md](SESSION_BOOTSTRAP.md).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
set -a && source .env && set +a
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 1. Session (M0 recap)
|
|
16
|
+
```bash
|
|
17
|
+
uv run teft bootstrap # expect ✓ Authenticated
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 2. M1 — content-search discovery → closes #9
|
|
21
|
+
```bash
|
|
22
|
+
uv run teft scrape --discover-only --dump -p "lost in meeting notes" -p "client follow-up"
|
|
23
|
+
```
|
|
24
|
+
- Expect a list of `/in/...` candidate URLs.
|
|
25
|
+
- **Good** → comment on #9, close it.
|
|
26
|
+
- **Empty/junk** → open `~/.teft/dump/anchors-*.json`; if the profile links are there
|
|
27
|
+
but under a different structure, the fix is the `main a[href]` scope / scroll in
|
|
28
|
+
`src/teft/search.py`. Paste me a dump and I'll adjust.
|
|
29
|
+
|
|
30
|
+
## 3. M2 — enrichment → closes #10
|
|
31
|
+
```bash
|
|
32
|
+
uv run teft scrape --dump -p "lost in meeting notes" -p "client follow-up" # writes ~/.teft/candidates.json
|
|
33
|
+
```
|
|
34
|
+
- Open `~/.teft/candidates.json`: are `name`, `headline`, `signal_post_text`
|
|
35
|
+
populated? `email`/`company` are best-effort (often empty for cold leads — fine).
|
|
36
|
+
- The **live selectors in `enrich.py` (`_scrape_one`) are guesses.** If fields are
|
|
37
|
+
blank, `~/.teft/dump/profile-*.json` has the raw captures — send me one and I'll
|
|
38
|
+
fix the `_NAME_JS` / `_HEADLINE_JS` / `_ACTIVITY_JS` selectors. This is the step
|
|
39
|
+
most likely to need a tuning round.
|
|
40
|
+
|
|
41
|
+
## 4. M3 + M4 — rank end-to-end → closes #11, #12 (the v0 done-condition)
|
|
42
|
+
```bash
|
|
43
|
+
uv run teft rank --brief tests/fixtures/humla_brief.txt # or your own brief file
|
|
44
|
+
```
|
|
45
|
+
- Reads `~/.teft/candidates.json`, writes `~/.teft/shortlist-<date>.md`.
|
|
46
|
+
- **The v0 bar:** is ≥1 entry send-worthy *unedited*? If yes → close #11 and #12; v0 is shipped.
|
|
47
|
+
- Tune loop is cheap: re-run `rank` freely (no re-scrape); only re-`scrape` for fresh candidates.
|
|
48
|
+
|
|
49
|
+
## Optional — validate the GPT-5.6 provider before trusting it
|
|
50
|
+
|
|
51
|
+
The SKIP-rule calibration is only proven on Claude (#4). To check GPT-5.6 gives the same
|
|
52
|
+
quality (Sara + Marcus surface, Tom's marathon SKIPs) before using it on real runs:
|
|
53
|
+
```bash
|
|
54
|
+
cp tests/fixtures/candidates.json ~/.teft/candidates.json
|
|
55
|
+
export OPENAI_API_KEY=...
|
|
56
|
+
TEFT_LLM=openai TEFT_MODEL=gpt-5.6-terra uv run teft rank --brief tests/fixtures/humla_brief.txt
|
|
57
|
+
```
|
|
58
|
+
Compare against the same run with the default (Claude). If GPT holds calibration, switch for real by
|
|
59
|
+
setting `TEFT_LLM=openai` in `.env`; if it force-fits or over-SKIPs, stay on Claude or try `gpt-5.6-sol`.
|
|
60
|
+
|
|
61
|
+
## If you'd rather I keep going now
|
|
62
|
+
Reply with a pasted dump file (anchors or profile JSON) and I'll tune selectors
|
|
63
|
+
blind against real DOM — that's the one thing that still needs your data, not your
|
|
64
|
+
presence.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Domain Docs
|
|
2
|
+
|
|
3
|
+
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
|
4
|
+
|
|
5
|
+
## Before exploring, read these
|
|
6
|
+
|
|
7
|
+
- **`CONTEXT.md`** at the repo root, or
|
|
8
|
+
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
|
9
|
+
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
|
|
10
|
+
|
|
11
|
+
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
|
12
|
+
|
|
13
|
+
## File structure
|
|
14
|
+
|
|
15
|
+
Single-context repo (most repos):
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
/
|
|
19
|
+
├── CONTEXT.md
|
|
20
|
+
├── docs/adr/
|
|
21
|
+
│ ├── 0001-event-sourced-orders.md
|
|
22
|
+
│ └── 0002-postgres-for-write-model.md
|
|
23
|
+
└── src/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/
|
|
30
|
+
├── CONTEXT-MAP.md
|
|
31
|
+
├── docs/adr/ ← system-wide decisions
|
|
32
|
+
└── src/
|
|
33
|
+
├── ordering/
|
|
34
|
+
│ ├── CONTEXT.md
|
|
35
|
+
│ └── docs/adr/ ← context-specific decisions
|
|
36
|
+
└── billing/
|
|
37
|
+
├── CONTEXT.md
|
|
38
|
+
└── docs/adr/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use the glossary's vocabulary
|
|
42
|
+
|
|
43
|
+
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
|
44
|
+
|
|
45
|
+
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
|
46
|
+
|
|
47
|
+
## Flag ADR conflicts
|
|
48
|
+
|
|
49
|
+
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
|
50
|
+
|
|
51
|
+
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|