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.
- karani-0.3.0/.circleci/config.yml +83 -0
- karani-0.3.0/.coverage +0 -0
- karani-0.3.0/.env.example +27 -0
- karani-0.3.0/.github/workflows/ci.yml +48 -0
- karani-0.3.0/.github/workflows/publish.yml +22 -0
- karani-0.3.0/.gitignore +30 -0
- karani-0.3.0/.mcp.json +8 -0
- karani-0.3.0/.python-version +1 -0
- karani-0.3.0/CLAUDE.md +363 -0
- karani-0.3.0/CONTRIBUTING.md +68 -0
- karani-0.3.0/LICENSE +21 -0
- karani-0.3.0/Makefile +59 -0
- karani-0.3.0/PKG-INFO +448 -0
- karani-0.3.0/README.md +378 -0
- karani-0.3.0/data/resume.md.example +37 -0
- karani-0.3.0/docker-compose.yml +68 -0
- karani-0.3.0/docs/adrs/0001-tiered-filter-not-single-llm-pass.md +35 -0
- karani-0.3.0/docs/adrs/0002-sf-band-global-remote-positioning.md +45 -0
- karani-0.3.0/docs/adrs/0003-openrouter-default-anthropic-optional.md +42 -0
- karani-0.3.0/docs/adrs/0004-normalized-content-hash.md +45 -0
- karani-0.3.0/docs/adrs/0005-state-machine-columns-not-table.md +45 -0
- karani-0.3.0/docs/adrs/0006-in-memory-storage-fallback.md +46 -0
- karani-0.3.0/docs/adrs/0007-agent-mode-optional-not-default.md +56 -0
- karani-0.3.0/docs/adrs/0008-mcp-server-interface.md +60 -0
- karani-0.3.0/docs/adrs/0009-memory-architecture.md +62 -0
- karani-0.3.0/docs/adrs/0010-slack-two-way-surface.md +54 -0
- karani-0.3.0/docs/adrs/0011-notion-mirror.md +50 -0
- karani-0.3.0/docs/adrs/0012-autopilot-draft-human-send.md +56 -0
- karani-0.3.0/docs/adrs/0013-langgraph-background-orchestration.md +62 -0
- karani-0.3.0/docs/adrs/0014-artifact-store-and-humanizer.md +62 -0
- karani-0.3.0/docs/adrs/0015-namespace-config-pypi.md +72 -0
- karani-0.3.0/docs/adrs/README.md +19 -0
- karani-0.3.0/docs/architecture.md +339 -0
- karani-0.3.0/docs/conventions.md +151 -0
- karani-0.3.0/docs/memory.md +136 -0
- karani-0.3.0/docs/operations.md +240 -0
- karani-0.3.0/docs/roadmap.md +466 -0
- karani-0.3.0/docs/vision.md +100 -0
- karani-0.3.0/karani/__init__.py +8 -0
- karani-0.3.0/karani/__main__.py +7 -0
- karani-0.3.0/karani/artifacts/__init__.py +12 -0
- karani-0.3.0/karani/artifacts/store.py +111 -0
- karani-0.3.0/karani/autopilot/__init__.py +12 -0
- karani-0.3.0/karani/autopilot/runner.py +108 -0
- karani-0.3.0/karani/cli.py +1048 -0
- karani-0.3.0/karani/config/__init__.py +14 -0
- karani-0.3.0/karani/config/loader.py +136 -0
- karani-0.3.0/karani/config/schema.py +145 -0
- karani-0.3.0/karani/config/wizard.py +122 -0
- karani-0.3.0/karani/drafting/__init__.py +17 -0
- karani-0.3.0/karani/drafting/followup.py +125 -0
- karani-0.3.0/karani/drafting/humanize.py +169 -0
- karani-0.3.0/karani/drafting/keywords.py +77 -0
- karani-0.3.0/karani/drafting/models.py +37 -0
- karani-0.3.0/karani/drafting/pipeline.py +132 -0
- karani-0.3.0/karani/drafting/prep.py +207 -0
- karani-0.3.0/karani/drafting/prompts.py +120 -0
- karani-0.3.0/karani/drafting/resume_tailor.py +142 -0
- karani-0.3.0/karani/drafting/runner.py +80 -0
- karani-0.3.0/karani/drafting/writers.py +87 -0
- karani-0.3.0/karani/ingestion/__init__.py +30 -0
- karani-0.3.0/karani/ingestion/aijobs.py +98 -0
- karani-0.3.0/karani/ingestion/ashby.py +101 -0
- karani-0.3.0/karani/ingestion/base.py +118 -0
- karani-0.3.0/karani/ingestion/config.py +169 -0
- karani-0.3.0/karani/ingestion/digest.py +270 -0
- karani-0.3.0/karani/ingestion/discovery.py +124 -0
- karani-0.3.0/karani/ingestion/filters.py +286 -0
- karani-0.3.0/karani/ingestion/greenhouse.py +62 -0
- karani-0.3.0/karani/ingestion/himalayas.py +122 -0
- karani-0.3.0/karani/ingestion/lever.py +61 -0
- karani-0.3.0/karani/ingestion/models.py +184 -0
- karani-0.3.0/karani/ingestion/orchestrator.py +163 -0
- karani-0.3.0/karani/ingestion/profile.py +59 -0
- karani-0.3.0/karani/ingestion/remoteok.py +120 -0
- karani-0.3.0/karani/ingestion/remotive.py +116 -0
- karani-0.3.0/karani/ingestion/resume.py +55 -0
- karani-0.3.0/karani/ingestion/roles.py +155 -0
- karani-0.3.0/karani/ingestion/sources.py +4 -0
- karani-0.3.0/karani/ingestion/storage.py +1383 -0
- karani-0.3.0/karani/ingestion/targets.py +115 -0
- karani-0.3.0/karani/ingestion/weworkremotely.py +86 -0
- karani-0.3.0/karani/ingestion/workable.py +135 -0
- karani-0.3.0/karani/intel/__init__.py +13 -0
- karani-0.3.0/karani/intel/service.py +176 -0
- karani-0.3.0/karani/mcp_server/__init__.py +6 -0
- karani-0.3.0/karani/mcp_server/__main__.py +21 -0
- karani-0.3.0/karani/mcp_server/server.py +657 -0
- karani-0.3.0/karani/memory/__init__.py +9 -0
- karani-0.3.0/karani/memory/manager.py +276 -0
- karani-0.3.0/karani/notionsync/__init__.py +14 -0
- karani-0.3.0/karani/notionsync/client.py +85 -0
- karani-0.3.0/karani/notionsync/sync.py +189 -0
- karani-0.3.0/karani/orchestration/__init__.py +13 -0
- karani-0.3.0/karani/orchestration/__main__.py +69 -0
- karani-0.3.0/karani/orchestration/graph.py +194 -0
- karani-0.3.0/karani/qualification/__init__.py +24 -0
- karani-0.3.0/karani/qualification/agent.py +181 -0
- karani-0.3.0/karani/qualification/anthropic.py +56 -0
- karani-0.3.0/karani/qualification/client.py +116 -0
- karani-0.3.0/karani/qualification/factory.py +50 -0
- karani-0.3.0/karani/qualification/local.py +50 -0
- karani-0.3.0/karani/qualification/models.py +48 -0
- karani-0.3.0/karani/qualification/openrouter.py +208 -0
- karani-0.3.0/karani/qualification/prompts.py +216 -0
- karani-0.3.0/karani/qualification/runner.py +124 -0
- karani-0.3.0/karani/qualification/tools.py +277 -0
- karani-0.3.0/karani/resources/docker-compose.yml +68 -0
- karani-0.3.0/karani/resources/karani.daily.plist.template +22 -0
- karani-0.3.0/karani/resources/karani.example.toml +45 -0
- karani-0.3.0/karani/resources/karani.hourly.plist.template +19 -0
- karani-0.3.0/karani/slackbridge/__init__.py +16 -0
- karani-0.3.0/karani/slackbridge/__main__.py +24 -0
- karani-0.3.0/karani/slackbridge/blocks.py +127 -0
- karani-0.3.0/karani/slackbridge/client.py +67 -0
- karani-0.3.0/karani/slackbridge/commands.py +329 -0
- karani-0.3.0/karani/slackbridge/interactions.py +79 -0
- karani-0.3.0/karani/slackbridge/listener.py +151 -0
- karani-0.3.0/karani.example.toml +45 -0
- karani-0.3.0/main.py +5 -0
- karani-0.3.0/pyproject.toml +63 -0
- karani-0.3.0/tests/__init__.py +0 -0
- karani-0.3.0/tests/conftest.py +89 -0
- karani-0.3.0/tests/test_actions_funnel.py +223 -0
- karani-0.3.0/tests/test_agent.py +111 -0
- karani-0.3.0/tests/test_artifacts_pipeline.py +193 -0
- karani-0.3.0/tests/test_autopilot.py +280 -0
- karani-0.3.0/tests/test_config.py +187 -0
- karani-0.3.0/tests/test_digest.py +62 -0
- karani-0.3.0/tests/test_discovery.py +27 -0
- karani-0.3.0/tests/test_drafting.py +79 -0
- karani-0.3.0/tests/test_e2e_pipeline.py +246 -0
- karani-0.3.0/tests/test_filters.py +174 -0
- karani-0.3.0/tests/test_humanize_tailor.py +138 -0
- karani-0.3.0/tests/test_intel.py +136 -0
- karani-0.3.0/tests/test_keywords.py +70 -0
- karani-0.3.0/tests/test_local_provider.py +70 -0
- karani-0.3.0/tests/test_mcp_server.py +424 -0
- karani-0.3.0/tests/test_memory.py +187 -0
- karani-0.3.0/tests/test_notion.py +224 -0
- karani-0.3.0/tests/test_orchestration.py +148 -0
- karani-0.3.0/tests/test_prep_followup.py +110 -0
- karani-0.3.0/tests/test_qualification.py +112 -0
- karani-0.3.0/tests/test_roles.py +46 -0
- karani-0.3.0/tests/test_slack.py +311 -0
- karani-0.3.0/tests/test_storage.py +141 -0
- 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
|
karani-0.3.0/.gitignore
ADDED
|
@@ -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 @@
|
|
|
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.
|