karani 0.3.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.
Files changed (147) hide show
  1. karani-0.3.0/.circleci/config.yml +83 -0
  2. karani-0.3.0/.coverage +0 -0
  3. karani-0.3.0/.env.example +27 -0
  4. karani-0.3.0/.github/workflows/ci.yml +48 -0
  5. karani-0.3.0/.github/workflows/publish.yml +22 -0
  6. karani-0.3.0/.gitignore +30 -0
  7. karani-0.3.0/.mcp.json +8 -0
  8. karani-0.3.0/.python-version +1 -0
  9. karani-0.3.0/CLAUDE.md +363 -0
  10. karani-0.3.0/CONTRIBUTING.md +68 -0
  11. karani-0.3.0/LICENSE +21 -0
  12. karani-0.3.0/Makefile +59 -0
  13. karani-0.3.0/PKG-INFO +448 -0
  14. karani-0.3.0/README.md +378 -0
  15. karani-0.3.0/data/resume.md.example +37 -0
  16. karani-0.3.0/docker-compose.yml +68 -0
  17. karani-0.3.0/docs/adrs/0001-tiered-filter-not-single-llm-pass.md +35 -0
  18. karani-0.3.0/docs/adrs/0002-sf-band-global-remote-positioning.md +45 -0
  19. karani-0.3.0/docs/adrs/0003-openrouter-default-anthropic-optional.md +42 -0
  20. karani-0.3.0/docs/adrs/0004-normalized-content-hash.md +45 -0
  21. karani-0.3.0/docs/adrs/0005-state-machine-columns-not-table.md +45 -0
  22. karani-0.3.0/docs/adrs/0006-in-memory-storage-fallback.md +46 -0
  23. karani-0.3.0/docs/adrs/0007-agent-mode-optional-not-default.md +56 -0
  24. karani-0.3.0/docs/adrs/0008-mcp-server-interface.md +60 -0
  25. karani-0.3.0/docs/adrs/0009-memory-architecture.md +62 -0
  26. karani-0.3.0/docs/adrs/0010-slack-two-way-surface.md +54 -0
  27. karani-0.3.0/docs/adrs/0011-notion-mirror.md +50 -0
  28. karani-0.3.0/docs/adrs/0012-autopilot-draft-human-send.md +56 -0
  29. karani-0.3.0/docs/adrs/0013-langgraph-background-orchestration.md +62 -0
  30. karani-0.3.0/docs/adrs/0014-artifact-store-and-humanizer.md +62 -0
  31. karani-0.3.0/docs/adrs/0015-namespace-config-pypi.md +72 -0
  32. karani-0.3.0/docs/adrs/README.md +19 -0
  33. karani-0.3.0/docs/architecture.md +339 -0
  34. karani-0.3.0/docs/conventions.md +151 -0
  35. karani-0.3.0/docs/memory.md +136 -0
  36. karani-0.3.0/docs/operations.md +240 -0
  37. karani-0.3.0/docs/roadmap.md +466 -0
  38. karani-0.3.0/docs/vision.md +100 -0
  39. karani-0.3.0/karani/__init__.py +8 -0
  40. karani-0.3.0/karani/__main__.py +7 -0
  41. karani-0.3.0/karani/artifacts/__init__.py +12 -0
  42. karani-0.3.0/karani/artifacts/store.py +111 -0
  43. karani-0.3.0/karani/autopilot/__init__.py +12 -0
  44. karani-0.3.0/karani/autopilot/runner.py +108 -0
  45. karani-0.3.0/karani/cli.py +1048 -0
  46. karani-0.3.0/karani/config/__init__.py +14 -0
  47. karani-0.3.0/karani/config/loader.py +136 -0
  48. karani-0.3.0/karani/config/schema.py +145 -0
  49. karani-0.3.0/karani/config/wizard.py +122 -0
  50. karani-0.3.0/karani/drafting/__init__.py +17 -0
  51. karani-0.3.0/karani/drafting/followup.py +125 -0
  52. karani-0.3.0/karani/drafting/humanize.py +169 -0
  53. karani-0.3.0/karani/drafting/keywords.py +77 -0
  54. karani-0.3.0/karani/drafting/models.py +37 -0
  55. karani-0.3.0/karani/drafting/pipeline.py +132 -0
  56. karani-0.3.0/karani/drafting/prep.py +207 -0
  57. karani-0.3.0/karani/drafting/prompts.py +120 -0
  58. karani-0.3.0/karani/drafting/resume_tailor.py +142 -0
  59. karani-0.3.0/karani/drafting/runner.py +80 -0
  60. karani-0.3.0/karani/drafting/writers.py +87 -0
  61. karani-0.3.0/karani/ingestion/__init__.py +30 -0
  62. karani-0.3.0/karani/ingestion/aijobs.py +98 -0
  63. karani-0.3.0/karani/ingestion/ashby.py +101 -0
  64. karani-0.3.0/karani/ingestion/base.py +118 -0
  65. karani-0.3.0/karani/ingestion/config.py +169 -0
  66. karani-0.3.0/karani/ingestion/digest.py +270 -0
  67. karani-0.3.0/karani/ingestion/discovery.py +124 -0
  68. karani-0.3.0/karani/ingestion/filters.py +286 -0
  69. karani-0.3.0/karani/ingestion/greenhouse.py +62 -0
  70. karani-0.3.0/karani/ingestion/himalayas.py +122 -0
  71. karani-0.3.0/karani/ingestion/lever.py +61 -0
  72. karani-0.3.0/karani/ingestion/models.py +184 -0
  73. karani-0.3.0/karani/ingestion/orchestrator.py +163 -0
  74. karani-0.3.0/karani/ingestion/profile.py +59 -0
  75. karani-0.3.0/karani/ingestion/remoteok.py +120 -0
  76. karani-0.3.0/karani/ingestion/remotive.py +116 -0
  77. karani-0.3.0/karani/ingestion/resume.py +55 -0
  78. karani-0.3.0/karani/ingestion/roles.py +155 -0
  79. karani-0.3.0/karani/ingestion/sources.py +4 -0
  80. karani-0.3.0/karani/ingestion/storage.py +1383 -0
  81. karani-0.3.0/karani/ingestion/targets.py +115 -0
  82. karani-0.3.0/karani/ingestion/weworkremotely.py +86 -0
  83. karani-0.3.0/karani/ingestion/workable.py +135 -0
  84. karani-0.3.0/karani/intel/__init__.py +13 -0
  85. karani-0.3.0/karani/intel/service.py +176 -0
  86. karani-0.3.0/karani/mcp_server/__init__.py +6 -0
  87. karani-0.3.0/karani/mcp_server/__main__.py +21 -0
  88. karani-0.3.0/karani/mcp_server/server.py +657 -0
  89. karani-0.3.0/karani/memory/__init__.py +9 -0
  90. karani-0.3.0/karani/memory/manager.py +276 -0
  91. karani-0.3.0/karani/notionsync/__init__.py +14 -0
  92. karani-0.3.0/karani/notionsync/client.py +85 -0
  93. karani-0.3.0/karani/notionsync/sync.py +189 -0
  94. karani-0.3.0/karani/orchestration/__init__.py +13 -0
  95. karani-0.3.0/karani/orchestration/__main__.py +69 -0
  96. karani-0.3.0/karani/orchestration/graph.py +194 -0
  97. karani-0.3.0/karani/qualification/__init__.py +24 -0
  98. karani-0.3.0/karani/qualification/agent.py +181 -0
  99. karani-0.3.0/karani/qualification/anthropic.py +56 -0
  100. karani-0.3.0/karani/qualification/client.py +116 -0
  101. karani-0.3.0/karani/qualification/factory.py +50 -0
  102. karani-0.3.0/karani/qualification/local.py +50 -0
  103. karani-0.3.0/karani/qualification/models.py +48 -0
  104. karani-0.3.0/karani/qualification/openrouter.py +208 -0
  105. karani-0.3.0/karani/qualification/prompts.py +216 -0
  106. karani-0.3.0/karani/qualification/runner.py +124 -0
  107. karani-0.3.0/karani/qualification/tools.py +277 -0
  108. karani-0.3.0/karani/resources/docker-compose.yml +68 -0
  109. karani-0.3.0/karani/resources/karani.daily.plist.template +22 -0
  110. karani-0.3.0/karani/resources/karani.example.toml +45 -0
  111. karani-0.3.0/karani/resources/karani.hourly.plist.template +19 -0
  112. karani-0.3.0/karani/slackbridge/__init__.py +16 -0
  113. karani-0.3.0/karani/slackbridge/__main__.py +24 -0
  114. karani-0.3.0/karani/slackbridge/blocks.py +127 -0
  115. karani-0.3.0/karani/slackbridge/client.py +67 -0
  116. karani-0.3.0/karani/slackbridge/commands.py +329 -0
  117. karani-0.3.0/karani/slackbridge/interactions.py +79 -0
  118. karani-0.3.0/karani/slackbridge/listener.py +151 -0
  119. karani-0.3.0/karani.example.toml +45 -0
  120. karani-0.3.0/main.py +5 -0
  121. karani-0.3.0/pyproject.toml +63 -0
  122. karani-0.3.0/tests/__init__.py +0 -0
  123. karani-0.3.0/tests/conftest.py +89 -0
  124. karani-0.3.0/tests/test_actions_funnel.py +223 -0
  125. karani-0.3.0/tests/test_agent.py +111 -0
  126. karani-0.3.0/tests/test_artifacts_pipeline.py +193 -0
  127. karani-0.3.0/tests/test_autopilot.py +280 -0
  128. karani-0.3.0/tests/test_config.py +187 -0
  129. karani-0.3.0/tests/test_digest.py +62 -0
  130. karani-0.3.0/tests/test_discovery.py +27 -0
  131. karani-0.3.0/tests/test_drafting.py +79 -0
  132. karani-0.3.0/tests/test_e2e_pipeline.py +246 -0
  133. karani-0.3.0/tests/test_filters.py +174 -0
  134. karani-0.3.0/tests/test_humanize_tailor.py +138 -0
  135. karani-0.3.0/tests/test_intel.py +136 -0
  136. karani-0.3.0/tests/test_keywords.py +70 -0
  137. karani-0.3.0/tests/test_local_provider.py +70 -0
  138. karani-0.3.0/tests/test_mcp_server.py +424 -0
  139. karani-0.3.0/tests/test_memory.py +187 -0
  140. karani-0.3.0/tests/test_notion.py +224 -0
  141. karani-0.3.0/tests/test_orchestration.py +148 -0
  142. karani-0.3.0/tests/test_prep_followup.py +110 -0
  143. karani-0.3.0/tests/test_qualification.py +112 -0
  144. karani-0.3.0/tests/test_roles.py +46 -0
  145. karani-0.3.0/tests/test_slack.py +311 -0
  146. karani-0.3.0/tests/test_storage.py +141 -0
  147. karani-0.3.0/uv.lock +3637 -0
@@ -0,0 +1,83 @@
1
+ # CircleCI mirror of .github/workflows/ci.yml — same commands, same gates.
2
+ # Publishing stays on GitHub Actions (publish.yml): PyPI Trusted
3
+ # Publishing is not available from CircleCI, and we do not store PyPI
4
+ # tokens in third-party CI secrets.
5
+ #
6
+ # One-time setup: circleci.com -> log in with GitHub -> Set Up Project
7
+ # for cod3smith/karani -> use this config. For the coverage badge, add
8
+ # COVERALLS_REPO_TOKEN in Project Settings -> Environment Variables
9
+ # (token from coveralls.io repo page).
10
+ version: 2.1
11
+
12
+ executors:
13
+ py:
14
+ parameters:
15
+ tag:
16
+ type: string
17
+ docker:
18
+ - image: cimg/python:<< parameters.tag >>
19
+
20
+ commands:
21
+ install:
22
+ steps:
23
+ - checkout
24
+ - run:
25
+ name: Install uv + project (all extras)
26
+ command: |
27
+ pip install --quiet uv
28
+ uv sync --all-extras
29
+
30
+ jobs:
31
+ test:
32
+ parameters:
33
+ tag:
34
+ type: string
35
+ executor:
36
+ name: py
37
+ tag: << parameters.tag >>
38
+ steps:
39
+ - install
40
+ - run:
41
+ name: Lint
42
+ command: uv run ruff check karani tests
43
+ - run:
44
+ name: Tests (with coverage)
45
+ command: >
46
+ uv run pytest tests -q
47
+ --cov=karani --cov-report=lcov:coverage.lcov --cov-report=term
48
+ - run:
49
+ name: Coveralls (3.13 only, when token configured)
50
+ command: |
51
+ if [ "<< parameters.tag >>" = "3.13" ] && [ -n "$COVERALLS_REPO_TOKEN" ]; then
52
+ uv run --with coveralls coveralls --service=circleci
53
+ else
54
+ echo "skipping coveralls upload"
55
+ fi
56
+
57
+ build:
58
+ executor:
59
+ name: py
60
+ tag: "3.13"
61
+ steps:
62
+ - checkout
63
+ - run:
64
+ name: Build wheel + sdist
65
+ command: |
66
+ pip install --quiet uv
67
+ uv build
68
+ - run:
69
+ name: Smoke-test the wheel
70
+ command: |
71
+ uv venv /tmp/smoke
72
+ uv pip install --python /tmp/smoke/bin/python dist/*.whl
73
+ /tmp/smoke/bin/karani --version
74
+ KARANI_HOME=/tmp/kh /tmp/smoke/bin/karani init --yes
75
+ KARANI_HOME=/tmp/kh KARANI_CONFIG=/tmp/kh/karani.toml \
76
+ /tmp/smoke/bin/karani config check
77
+
78
+ workflows:
79
+ ci:
80
+ jobs:
81
+ - test:
82
+ tag: "3.13"
83
+ - build
karani-0.3.0/.coverage ADDED
Binary file
@@ -0,0 +1,27 @@
1
+ # karani secrets — copy to .env and fill in. NEVER commit .env.
2
+ # Everything non-secret (what to hunt, providers, endpoints, channels)
3
+ # lives in karani.toml — create yours with: karani init
4
+
5
+ # Postgres. Unset = in-memory storage (state lost between runs).
6
+ DATABASE_URL=postgresql://karani:karani@localhost:5433/karani
7
+
8
+ # LLM provider keys — only the provider(s) you use in karani.toml [llm].
9
+ OPENROUTER_API_KEY=sk-or-v1-your-key-here
10
+ # ANTHROPIC_API_KEY=sk-ant-...
11
+ # (provider "local" needs no key — any OpenAI-compatible server)
12
+
13
+ # Slack (two-way surface; channel id lives in karani.toml [slack])
14
+ # SLACK_BOT_TOKEN=xoxb-... # OAuth & Permissions -> Bot User OAuth Token
15
+ # SLACK_APP_TOKEN=xapp-... # Basic Information -> App-Level Tokens
16
+
17
+ # Notion mirror (database id lives in karani.toml [notion])
18
+ # NOTION_TOKEN=ntn_... # notion.so/my-integrations
19
+
20
+ # MinIO artifact store (endpoint lives in karani.toml [artifacts])
21
+ # MINIO_ACCESS_KEY=karani
22
+ # MINIO_SECRET_KEY=karani-local-secret
23
+
24
+ # Machine knobs (optional)
25
+ # RESUME_PATH=data/resume.md
26
+ # HTTP_CONCURRENCY=6
27
+ # STALE_JOB_DAYS=10
@@ -0,0 +1,48 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: uv sync --all-extras
22
+ - name: Lint
23
+ run: uv run ruff check karani tests
24
+ - name: Tests (with coverage)
25
+ run: >
26
+ uv run pytest tests -q
27
+ --cov=karani --cov-report=lcov:coverage.lcov --cov-report=term
28
+ - name: Coveralls
29
+ if: matrix.python-version == '3.13'
30
+ uses: coverallsapp/github-action@v2
31
+ with:
32
+ file: coverage.lcov
33
+
34
+ build:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: astral-sh/setup-uv@v5
39
+ - name: Build wheel + sdist
40
+ run: uv build
41
+ - name: Smoke-test the wheel
42
+ run: |
43
+ uv venv /tmp/smoke
44
+ uv pip install --python /tmp/smoke/bin/python dist/*.whl
45
+ /tmp/smoke/bin/karani --version
46
+ KARANI_HOME=/tmp/kh /tmp/smoke/bin/karani init --yes
47
+ KARANI_HOME=/tmp/kh KARANI_CONFIG=/tmp/kh/karani.toml \
48
+ /tmp/smoke/bin/karani config check
@@ -0,0 +1,22 @@
1
+ name: publish
2
+
3
+ # Publishes to PyPI on a GitHub release, via PyPI Trusted Publishing
4
+ # (OIDC) — no API token stored in repo secrets. One-time setup on
5
+ # pypi.org: project karani -> Publishing -> add this repo + workflow.
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ pypi:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+ - name: Build
20
+ run: uv build
21
+ - name: Publish to PyPI
22
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,30 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Env / secrets
13
+ .env
14
+ .env.local
15
+ *.pem
16
+
17
+ # OS noise
18
+ .DS_Store
19
+
20
+ # Personal data — resume.md.example is the committed template
21
+ data/resume.md
22
+
23
+ # Generated output
24
+ data/digest.html
25
+ drafts/
26
+ logs/
27
+
28
+ # Personal hunt configuration — karani.example.toml ships in the wheel
29
+ karani.toml
30
+ .env.backup
karani-0.3.0/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "karani": {
4
+ "command": "uv",
5
+ "args": ["run", "karani", "mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ 3.13
karani-0.3.0/CLAUDE.md ADDED
@@ -0,0 +1,363 @@
1
+ # CLAUDE.md — karani
2
+
3
+ This file is the entry point for any Claude Code (or coding-agent) session on this
4
+ repo. Read it first, then follow the pointers into `docs/` for depth.
5
+
6
+ If you change something material — architecture, positioning, a public interface,
7
+ or a rule below — update this file in the same commit. Docs drift is worse than
8
+ no docs.
9
+
10
+ ---
11
+
12
+ ## 1. What this is
13
+
14
+ **karani** is a semi-autonomous, personal job-application pipeline for **Kelyn
15
+ Njeri** (Nairobi-based senior/staff engineer with a data-platform + causal-ML
16
+ background). It:
17
+
18
+ 1. **Ingests** postings from 9 sources (Greenhouse, Lever, Ashby, Workable,
19
+ RemoteOK, Himalayas, We Work Remotely, Remotive, aijobs.net).
20
+ 2. **Pre-filters** deterministically for role fit + seniority + geo + comp +
21
+ skill overlap. Drops ~95% before any LLM call.
22
+ 3. **Qualifies** survivors against Kelyn's resume via LLM (OpenRouter →
23
+ `moonshotai/kimi-k2-thinking` by default; single-turn or tool-using agent
24
+ loop). Emits `fit_score`, `verdict`, evidence-backed strengths, gaps,
25
+ recommended positioning.
26
+ 4. **Digests** the shortlist as HTML / markdown / text.
27
+ 5. **Drafts** cover letters + tailored bullets + application-question answers
28
+ in Kelyn's voice.
29
+ 6. **Tracks** the application through a state machine (drafting → applied →
30
+ interviewing → offer/rejection).
31
+ 7. **Learns** from Kelyn's reactions via a `user_verdict` feedback loop that
32
+ feeds past pairs back as few-shot examples on the next qualify run.
33
+ 8. **Serves** the whole pipeline as an MCP server (`karani/mcp_server/`, stdio) so
34
+ MCP clients can drive ingest/qualify/shortlist/draft/track
35
+ conversationally. See `docs/adrs/0008-mcp-server-interface.md`.
36
+ 9. **Remembers** distilled facts (preferences, company intel, outcomes)
37
+ in a ledger-first memory layer (`karani/memory/`) and injects them into
38
+ qualification. mem0 + pgvector optional upgrade. See `docs/memory.md`.
39
+ 10. **Converses** over Slack (`karani/slackbridge/`, Socket Mode): pushes the
40
+ digest/worklist and takes the same verbs back (`verdict 123 apply`,
41
+ `prep 45`). See `docs/adrs/0010-slack-two-way-surface.md`.
42
+ 11. **Mirrors** tracked applications onto a Notion board
43
+ (`karani/notionsync/`), updated live on every state change and reconciled
44
+ by the scheduled run. See `docs/adrs/0011-notion-mirror.md`.
45
+ 12. **Hunts** continuously (`karani/autopilot/`, `karani hunt`): drafts packs
46
+ for top-fit roles unattended (fit floor + per-run cap) and delivers
47
+ Slack review cards with Approve/Skip/Applied buttons. Never submits
48
+ — see `docs/adrs/0012-autopilot-draft-human-send.md`.
49
+ 13. **Converts**: fast-lane flags on fresh high-fit roles, ATS keyword
50
+ coverage per draft, cached company dossiers (`karani/intel/`), warm-path
51
+ candidates, interview prep packs, and dossier-hooked follow-ups —
52
+ all measured through `funnel_stats` (roadmap Tier 1.5).
53
+
54
+ **Positioning:** two role shapes qualify — (1) *companies that hire
55
+ globally at SF pay bands regardless of candidate location*, and
56
+ (2) *roles that sponsor a visa + relocation, EU and Japan preferred*
57
+ (local top-of-market comp acceptable there). Target roles: software
58
+ engineering, research engineering, ML/AI — computational-bio roles are
59
+ excluded. See `docs/vision.md`.
60
+
61
+ ---
62
+
63
+ ## 2. Repo layout
64
+
65
+ ```
66
+ karani/ (repo)
67
+ ├── CLAUDE.md ← you are here
68
+ ├── README.md ← human quickstart (pip install karani)
69
+ ├── pyproject.toml ← PyPI package `karani`; `karani` CLI script
70
+ ├── Makefile ← thin dev aliases over the karani CLI
71
+ ├── karani.example.toml → shipped in-wheel; see karani/resources/
72
+ ├── .env.example ← secrets template (keys live in env, never toml)
73
+
74
+ ├── karani/ ← THE installable package
75
+ │ ├── cli.py ← `karani <verb>` — ~30 verbs, single entry point
76
+ │ ├── config/ ← karani.toml schema + loader + `karani init` wizard (ADR 0015)
77
+ │ ├── resources/ ← compose file, launchd templates, example toml (in-wheel)
78
+ │ ├── ingestion/ ← fetch → classify → pre-filter → store → sweep
79
+ │ │ (config.py signals · profile.py · filters.py · roles.py · storage.py ·
80
+ │ │ targets.py · orchestrator.py · digest.py · discovery.py · resume.py ·
81
+ │ │ per-source fetchers: greenhouse/lever/ashby/workable + 5 feeds)
82
+ │ ├── qualification/ ← LLM fit tier: prompts (versioned) · client ·
83
+ │ │ providers (openrouter/anthropic/local) · factory (per-task routing) ·
84
+ │ │ tools + agent loop · runner
85
+ │ ├── drafting/ ← pack factory: pipeline.py (draft→humanize→
86
+ │ │ tailor→store, ALL surfaces) · keywords · humanize · resume_tailor ·
87
+ │ │ prep · followup · writers
88
+ │ ├── intel/ ← cached company dossiers + warm paths (TTL 14d)
89
+ │ ├── memory/ ← ledger-first memory, mem0 optional (docs/memory.md)
90
+ │ ├── slackbridge/ ← two-way Slack: client/blocks/commands/
91
+ │ │ interactions (pack buttons)/listener (ADR 0010)
92
+ │ ├── notionsync/ ← Notion board mirror, schema-adopting (ADR 0011)
93
+ │ ├── autopilot/ ← continuous hunt: pack + review card (ADR 0012)
94
+ │ ├── orchestration/ ← LangGraph hourly pass (ADR 0013)
95
+ │ ├── artifacts/ ← MinIO/S3 per-job objects + presigned links (ADR 0014)
96
+ │ └── mcp_server/ ← MCP interface, 25 tools, thin adapter (ADR 0008)
97
+
98
+ ├── docker-compose.yml ← dev infra (pgvector db :5433, minio :9010, ollama)
99
+ ├── data/ ← resume.md (yours, gitignored) + .example
100
+ ├── drafts/ ← generated packs (gitignored)
101
+ ├── tests/ ← 192+ deterministic tests (no network, no clock)
102
+ └── docs/ ← vision · architecture · roadmap · memory ·
103
+ conventions · operations · adrs/ (0001-0015)
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 3. Getting oriented in 60 seconds
109
+
110
+ ```bash
111
+ # One-time setup
112
+ uv sync # or pip install -e .
113
+ uv sync --extra dev # + pytest
114
+ cp .env.example .env # fill DATABASE_URL, OPENROUTER_API_KEY
115
+ cp data/resume.md.example data/resume.md # edit it — this is YOU
116
+
117
+ # Daily loop
118
+ karani hourly # one full hunt pass
119
+ open data/digest.html # or send it to yourself
120
+
121
+ # Act on a suggestion
122
+ karani draft 12345 # writes drafts/*.md
123
+ karani status 12345 applied
124
+ karani verdict 12345 apply # feeds the taste-calibration loop
125
+
126
+ # Or drive it all over MCP (Claude Code picks up .mcp.json automatically)
127
+ karani mcp # MCP server (stdio)
128
+ ```
129
+
130
+ Full CLI reference is in `README.md`. Full command surface is in `Makefile`.
131
+
132
+ ---
133
+
134
+ ## 4. Non-negotiable rules
135
+
136
+ These aren't preferences — they're guardrails you must respect. Break one, and
137
+ you'll silently degrade the pipeline in a way that's hard to spot.
138
+
139
+ ### 4.1 Never regress the SF-band, global-remote thesis
140
+ - `ingestion/config.py` signals are named `global_hire_*`, `regional_restriction_*`,
141
+ `pay_parity_*`. **Do not** reintroduce Kenya-specific names — they're the whole
142
+ reason the pipeline exists, but the *filter* is location-agnostic (a US-only
143
+ role gets vetoed whether Kelyn's in Nairobi, Berlin, or Buenos Aires).
144
+ - `min_comp_usd` default is $160k. Below that = not SF-band = drop.
145
+ - **Extension (2026-08, prompts qual-v3):** `relocation_signals` soften
146
+ geo/onsite vetos — a region-locked or onsite role that sponsors visa +
147
+ relocation (EU/Japan preferred) goes to the LLM instead of being
148
+ dropped, and the LLM may accept local top-of-market comp for those.
149
+ This *adds* a second qualifying shape; it does not weaken shape one.
150
+
151
+ ### 4.2 Signal matching is word-boundary anchored
152
+ - Use `_find_signal` in `filters.py` (or the `_wb()` helper in `roles.py`).
153
+ - **Never** substring-match location or role phrases. "us only" matches "campus
154
+ only" as a substring — that's the exact bug that made the pipeline useless
155
+ before the refactor. There's a regression test in `tests/test_filters.py`.
156
+
157
+ ### 4.3 Comp parsing requires a currency anchor
158
+ - `_parse_comp` in `filters.py` only accepts a number band when a currency /
159
+ comp keyword is within 40 chars. Do not weaken this — you'll parse "5-10
160
+ years experience" as $5k-$10k and hard-fail every role.
161
+
162
+ ### 4.4 Content hash is normalized
163
+ - `Job.compute_hash` lowercases + strips punctuation + collapses whitespace
164
+ before hashing. If you change the hash inputs, previously-qualified jobs
165
+ will *all* re-qualify (billed). If that's intentional, say so in the commit.
166
+
167
+ ### 4.5 Every fetcher must go through `get_with_retry`
168
+ - `ingestion/base.py`. Enforces per-host semaphore + tenacity backoff + 404
169
+ fast-fail. Do not write a fetcher that calls `client.get` directly.
170
+
171
+ ### 4.6 Every fetched `Job` must call `.finalize()`
172
+ - Populates `content_hash` and `canonical_hash`. Without these, upsert dedup
173
+ and cross-source dedup silently break.
174
+
175
+ ### 4.7 Storage schema changes go through `ALTER TABLE ... IF NOT EXISTS`
176
+ - `ingestion/storage.py` runs the `SCHEMA` block on every connect. It's
177
+ idempotent. Never write a destructive migration — Kelyn's Neon DB has
178
+ history in it now.
179
+
180
+ ### 4.8 Credentials come from env vars only
181
+ - `config.py` reads `os.getenv(...)`. Never hardcode a DSN, API key, or token
182
+ in code. The `.env.example` shows the shape.
183
+
184
+ ### 4.9 The pre-filter is deterministic
185
+ - No LLM calls in `filters.py` or `roles.py`. That's the whole point of the
186
+ tier split — pre-filter is free and fast, qualification is billed. Don't
187
+ cross the streams.
188
+
189
+ ### 4.10 Prompts are versioned
190
+ - `qualification/prompts.py` has `PROMPT_VERSION`, `AGENT_PROMPT_VERSION`.
191
+ `drafting/prompts.py` has `DRAFT_PROMPT_VERSION`. Bump them when you
192
+ materially change a prompt — persisted `QualificationResult` rows carry
193
+ the version, and that's how we distinguish "old data" from "new data" for
194
+ A/B and rollback.
195
+
196
+ ### 4.11 The memory ledger is the system of record
197
+ - The `memories` table in Postgres is ground truth; the mem0/pgvector
198
+ index is derived and disposable. Never write memory only to mem0, and
199
+ never let a mem0 failure abort a batch — degrade to `basic` recall.
200
+ See `docs/memory.md` and ADR 0009.
201
+ - Memory writes happen on explicit events (verdict, outcome, deliberate
202
+ `remember`) — never auto-extracted from arbitrary LLM output.
203
+
204
+ ---
205
+
206
+ ## 5. Coding conventions
207
+
208
+ Full details in `docs/conventions.md`. The one-liners:
209
+
210
+ - **Python 3.11+**, no exceptions.
211
+ - **Async everywhere** in ingestion + qualification. Blocking `time.sleep` or
212
+ sync `requests` calls will break the concurrency model.
213
+ - **Pydantic v2** for every structured payload that crosses a boundary.
214
+ - **`from __future__ import annotations`** at the top of every module.
215
+ - **Type hints everywhere.** Use `dict`, `list`, `str | None` — not
216
+ `Dict`, `List`, `Optional`.
217
+ - **Guard optional imports.** `qualification/anthropic.py` imports the
218
+ Anthropic SDK inside `__init__` so the module loads even when the SDK
219
+ isn't installed. Follow the same pattern for any future optional dep.
220
+ - **Errors from LLMs are recoverable.** `qualify_one` on malformed JSON
221
+ downgrades to `verdict="maybe"` with a `why_skip` explanation. Never let
222
+ a bad LLM response throw and kill a batch.
223
+ - **No emojis in code or docs** unless explicitly requested.
224
+
225
+ ---
226
+
227
+ ## 6. Testing conventions
228
+
229
+ - Run: `make test` or `uv run pytest tests -q`. CI runs the same
230
+ suite + ruff + a wheel smoke on 3.11/3.12/3.13, with coverage to
231
+ Coveralls (`.github/workflows/ci.yml`).
232
+ - Every new module needs a smoke test at minimum.
233
+ - All 158 existing tests are deterministic (no network, no clock). Keep it
234
+ that way — use fake clients for LLM calls (see `tests/test_qualification.py`
235
+ and `tests/test_agent.py`), and `httpx.MockTransport` for HTTP (see
236
+ `tests/test_e2e_pipeline.py`).
237
+ - MCP tools are tested through `app.call_tool` — full validation +
238
+ execution + serialization, no transport (see `tests/test_mcp_server.py`).
239
+ - **conftest.py strips all external-service credentials** (Notion, Slack,
240
+ DATABASE_URL) before any module loads, plus an autouse re-strip per
241
+ test. Best-effort integrations read env at call time — without the
242
+ strip, the developer's real .env makes "deterministic" tests write to
243
+ live services (this happened: 30 fixture pages on the real Notion
244
+ board). A suite that suddenly runs slower than ~2s is doing network
245
+ I/O — treat that as a failure even when everything passes.
246
+ - `pytest-asyncio` is auto-mode (see `pyproject.toml`). Just decorate coroutines
247
+ with `@pytest.mark.asyncio`.
248
+ - The in-memory `Storage` fallback (`Storage("")`) is the standard test
249
+ substrate — no Postgres required.
250
+
251
+ ---
252
+
253
+ ## 7. Where to work next
254
+
255
+ Prioritized list is in **`docs/roadmap.md`** with acceptance criteria per item.
256
+ Summary:
257
+
258
+ 1. **Cron / scheduled runs** — `Makefile` is ready; wire it to `launchd`
259
+ (mac) or `systemd`/`cron` (linux). Half-day.
260
+ 2. **Neon credential rotation** — the DSN was in `config.py` at one point
261
+ before the refactor. Rotate it. 10 minutes on Kelyn's side, but has to
262
+ happen before real ops.
263
+ 3. **Persistent per-source health metrics** — right now the run stats are
264
+ ephemeral. Add a `source_runs` table + a `stats history` CLI. Half-day.
265
+ 4. **Digest as email / Slack / Cowork artifact** — HTML file exists. Wire
266
+ one delivery channel (SES, Slack webhook, or a Cowork artifact). Day.
267
+ 5. **Levels.fyi comp overlay** — where comp isn't disclosed, backfill via
268
+ a scrape/API. Feeds `fit_score`. Day.
269
+ 6. **Agent tool-use expansion** — add `check_glassdoor_reputation`,
270
+ `fetch_recent_news`, `check_engineering_blog_recency`. Half-day per tool.
271
+
272
+ Bigger unlocks in `docs/roadmap.md`.
273
+
274
+ ---
275
+
276
+ ## 8. Where to find the "why" for decisions
277
+
278
+ `docs/adrs/` — one file per key decision, why-based (context + decision +
279
+ consequences). Don't argue architecture with the code; argue it with an ADR.
280
+ If you're about to reverse a decision documented in an ADR, add a new ADR
281
+ that supersedes it. Never delete.
282
+
283
+ ---
284
+
285
+ ## 9. Working with Kelyn
286
+
287
+ - Kelyn is a **staff/principal-band engineer** (see `data/resume.md`). Talk
288
+ to him accordingly — skip explaining Python basics, but do explain
289
+ domain-specific tradeoffs (ATS quirks, prompt-engineering choices, DB
290
+ schema evolution).
291
+ - Timezone: **EAT (UTC+3)**. Async-friendly.
292
+ - Preferences: **concise, direct, no fluff, no emojis unless he asks.**
293
+ Prose over bullet lists in normal conversation; bullet lists for real
294
+ structured deliverables (this doc, roadmap, ADRs).
295
+ - He'll push back if you sugar-coat. Be blunt about tradeoffs.
296
+
297
+ ---
298
+
299
+ ## 10. Fast paths for common tasks
300
+
301
+ ### Add a new ingestion source
302
+ 1. Add enum to `Source` in `ingestion/models.py`.
303
+ 2. Write `ingestion/<name>.py` — subclass `Fetcher`, use `get_with_retry`,
304
+ call `.finalize()` on every `Job`.
305
+ 3. Register in `ingestion/__init__.py:FETCHERS`.
306
+ 4. If per-company: add slugs to `TARGETS` in `targets.py`. If feed: add to
307
+ `FEED_SOURCES`.
308
+ 5. Write a test with a fake JSON payload.
309
+
310
+ ### Change what the pre-filter drops
311
+ 1. Edit `ingestion/filters.py` `pre_filter()`.
312
+ 2. Add / edit signals in `ingestion/config.py`.
313
+ 3. Add a regression test in `tests/test_filters.py`.
314
+ 4. If the change affects role classification: also touch `ingestion/roles.py`
315
+ and `tests/test_roles.py`.
316
+
317
+ ### Add a new LLM tool for the agent
318
+ 1. Add a function to `qualification/tools.py` — return text, wrap errors,
319
+ respect the byte cap.
320
+ 2. Append a `Tool(...)` entry to `DEFAULT_TOOLS`.
321
+ 3. Update the agent system prompt in `qualification/prompts.py`
322
+ (`AGENT_SYSTEM_PROMPT`) and bump `AGENT_PROMPT_VERSION`.
323
+ 4. Write a smoke test that scripts a `chat_turn` returning a `tool_calls`
324
+ block invoking your tool.
325
+
326
+ ### Add a new MCP tool
327
+ 1. Put any new query/mutation on `Storage` (or a runner) first — the MCP
328
+ layer stays a thin adapter, same as the CLI.
329
+ 2. Add an `@app.tool()` function in `mcp_server/server.py`. Raise `ToolError`
330
+ for expected user-input failures (plain exceptions get masked by the SDK).
331
+ 3. Add a test in `tests/test_mcp_server.py` via `app.call_tool`, and update
332
+ the tool-set assertion in `test_tool_listing`.
333
+ 4. If the tool mirrors a CLI verb, keep the two surfaces in sync.
334
+
335
+ ### Swap LLM providers
336
+ 1. Either set `QUAL_PROVIDER=anthropic|local` / `QUAL_MODEL=...` in env, or
337
+ pass `--provider` / `--model` to `qualify` and `draft`. `local` speaks to
338
+ any OpenAI-compatible server (Ollama/LM Studio/vLLM) — zero token cost;
339
+ recommended for bulk qualify, with a hosted model kept for drafting.
340
+ 2. To add a *new* provider: subclass into `qualification/<provider>.py`,
341
+ implement `complete()` (and `chat_turn()` if you want agent-mode
342
+ support), register in `qualification/factory.py`.
343
+
344
+ ---
345
+
346
+ ## 11. Things that look wrong but aren't
347
+
348
+ - **`ingestion/sources.py` re-exports `FETCHERS` from `__init__.py`.** Legacy
349
+ compat shim. Don't delete without grep — external scripts might import
350
+ from there.
351
+ - **`main.py` at root delegates to `ingestion.cli.main`.** Same reason.
352
+ - **`comp_currency_original="USD"` when comp is undisclosed and unknown.**
353
+ Deliberate — the RemoteOK feed omits currency, so we accept as USD only
354
+ when the number falls in a plausible US-band range. See the docstring in
355
+ `remoteok.py`.
356
+ - **`stages` is a JSONB append log, not a normalized `interview_stages`
357
+ table.** Deliberate simplicity — see `docs/adrs/`.
358
+
359
+ ---
360
+
361
+ Last audit: this file must be re-read whenever any of the following change:
362
+ `pyproject.toml`, `ingestion/config.py`, `ingestion/models.py`,
363
+ `qualification/prompts.py`, or `docs/roadmap.md`.
@@ -0,0 +1,68 @@
1
+ # Contributing to karani
2
+
3
+ karani is a personal, semi-autonomous job-application pipeline built in
4
+ public. Contributions are welcome — the architecture is deliberately
5
+ modular so most changes touch one package.
6
+
7
+ ## Ground rules (read these first)
8
+
9
+ 1. **`CLAUDE.md` section 4 is non-negotiable.** Word-boundary signal
10
+ matching, deterministic pre-filter (no LLM calls in `filters.py`),
11
+ versioned prompts, ledger-first memory, idempotent schema migrations.
12
+ PRs that break a guardrail get closed with a pointer to the rule.
13
+ 2. **Karani never submits an application.** Anything that auto-submits,
14
+ auto-messages recruiters, or bypasses human review is out of scope —
15
+ see `docs/vision.md` non-goals and ADR 0012.
16
+ 3. **Argue architecture with ADRs, not code.** Reversing a documented
17
+ decision needs a superseding ADR in `docs/adrs/`.
18
+ 4. **Every surface stays in sync.** A new verb lands on `Storage`/runners
19
+ first, then CLI + MCP + Slack together (see ADR 0008/0010).
20
+
21
+ ## Setup
22
+
23
+ ```bash
24
+ git clone <repo> && cd karani
25
+ uv sync --all-extras
26
+ cp .env.example .env # fill what you use; all optional
27
+ cp data/resume.md.example data/resume.md # your resume — never committed
28
+ make test # 181+ tests, ~2s, no network
29
+ ```
30
+
31
+ The whole pipeline runs with zero external services (in-memory storage,
32
+ `basic` memory mode). Postgres/Slack/Notion/MinIO/Ollama are additive.
33
+
34
+ ## Testing conventions
35
+
36
+ - Deterministic only: no network, no clock. Fake LLM clients (see
37
+ `tests/test_qualification.py`), `httpx.MockTransport` for HTTP,
38
+ `Storage("")` for the DB. `conftest.py` strips all real credentials —
39
+ a suite that runs slower than ~2s is doing network I/O and is wrong.
40
+ - Every new module needs at least a smoke test; every bugfix needs the
41
+ regression test that would have caught it.
42
+ - MCP tools test through `app.call_tool`; Slack verbs through
43
+ `handle_command`; graph nodes through `build_hunt_graph` with fakes.
44
+
45
+ ## Where to start
46
+
47
+ `docs/roadmap.md` is the single source of planned work, with acceptance
48
+ criteria per item. Good first contributions:
49
+
50
+ - **Tier 0 (production hardening)** — well-scoped, high-impact items
51
+ from the 2026-08 audit: cross-run dedup in candidate queries, advisory
52
+ locks around billed runs, a Postgres-marked test suite, heartbeat +
53
+ run/cost ledger, per-task model routing.
54
+ - **A new ingestion source** — the most self-contained change there is;
55
+ recipe in `CLAUDE.md` section 10.
56
+ - **Agent tools** (`qualification/tools.py`) — each tool is one function,
57
+ one registry entry, one smoke test.
58
+
59
+ ## PR checklist
60
+
61
+ - [ ] `make test` green, `uv run ruff check` clean on touched files
62
+ (CI enforces both on 3.11–3.13 plus a wheel install smoke —
63
+ GitHub Actions and CircleCI run the same commands)
64
+ - [ ] Guardrails in `CLAUDE.md` section 4 respected
65
+ - [ ] Prompt changed materially? Version bumped
66
+ - [ ] New capability? Landed on Storage/runner first, all surfaces synced
67
+ - [ ] Docs updated in the same PR (`CLAUDE.md` rule: drift is worse than
68
+ no docs)
karani-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kelyn Njeri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.