31 KiB
circuitforge-core Extraction Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Extract the shared scaffold from Peregrine into a standalone circuitforge-core Python package, update Peregrine to depend on it, and leave Peregrine's behaviour unchanged.
Architecture: New private repo circuitforge-core at /Library/Development/CircuitForge/circuitforge-core/. Contains: db base connection, LLM router, tier system, and config loader — the minimum core needed for both Peregrine and Snipe. Vision module, wizard framework, and pipeline module are stubbed (vision and wizard are net-new; pipeline is extracted but task-runner is deferred). Peregrine's requirements.txt gets a -e ../circuitforge-core entry; all internal imports updated.
Tech Stack: Python 3.11+, SQLite (stdlib), pytest, pyproject.toml (core only — Peregrine stays on requirements.txt)
File Map
New files (circuitforge-core repo)
| File | Responsibility |
|---|---|
circuitforge_core/__init__.py |
Package version export |
circuitforge_core/db/__init__.py |
Re-exports get_connection |
circuitforge_core/db/base.py |
get_connection() — SQLite/SQLCipher connection factory (extracted from peregrine/scripts/db.py) |
circuitforge_core/db/migrations.py |
run_migrations(conn, migrations_dir) — simple sequential migration runner |
circuitforge_core/llm/__init__.py |
Re-exports LLMRouter |
circuitforge_core/llm/router.py |
LLMRouter class (extracted from peregrine/scripts/llm_router.py) |
circuitforge_core/vision/__init__.py |
Re-exports VisionRouter |
circuitforge_core/vision/router.py |
VisionRouter stub — raises NotImplementedError until v0.2 |
circuitforge_core/wizard/__init__.py |
Re-exports BaseWizard |
circuitforge_core/wizard/base.py |
BaseWizard stub — raises NotImplementedError until first product wires it |
circuitforge_core/pipeline/__init__.py |
Re-exports StagingDB |
circuitforge_core/pipeline/staging.py |
StagingDB stub — interface for SQLite staging queue; raises NotImplementedError |
circuitforge_core/tiers/__init__.py |
Re-exports can_use, TIERS, BYOK_UNLOCKABLE, LOCAL_VISION_UNLOCKABLE |
circuitforge_core/tiers/tiers.py |
Generalised tier system (extracted from peregrine/app/wizard/tiers.py, product-specific feature keys removed) |
circuitforge_core/config/__init__.py |
Re-exports require_env, load_env |
circuitforge_core/config/settings.py |
require_env(key), load_env(path) — env validation helpers |
pyproject.toml |
Package metadata, no dependencies beyond stdlib + pyyaml + requests + openai |
tests/test_db.py |
Tests for get_connection and migration runner |
tests/test_tiers.py |
Tests for can_use, BYOK unlock, LOCAL_VISION unlock |
tests/test_llm_router.py |
Tests for LLMRouter fallback chain (mock backends) |
tests/test_config.py |
Tests for require_env missing/present |
Modified files (Peregrine repo)
| File | Change |
|---|---|
requirements.txt |
Add -e ../circuitforge-core |
scripts/db.py |
Replace get_connection body with from circuitforge_core.db import get_connection; ... re-export |
scripts/llm_router.py |
Replace LLMRouter class body with import from circuitforge_core.llm |
app/wizard/tiers.py |
Replace tier/BYOK logic with imports from circuitforge_core.tiers; keep Peregrine-specific FEATURES dict |
Task 1: Scaffold circuitforge-core repo
Files:
-
Create:
circuitforge-core/pyproject.toml -
Create:
circuitforge-core/circuitforge_core/__init__.py -
Create:
circuitforge-core/.gitignore -
Create:
circuitforge-core/README.md -
Step 1: Create repo directory and init git
mkdir -p /Library/Development/CircuitForge/circuitforge-core
cd /Library/Development/CircuitForge/circuitforge-core
git init
- Step 2: Write pyproject.toml
# /Library/Development/CircuitForge/circuitforge-core/pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "circuitforge-core"
version = "0.1.0"
description = "Shared scaffold for CircuitForge products"
requires-python = ">=3.11"
dependencies = [
"pyyaml>=6.0",
"requests>=2.31",
"openai>=1.0",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["circuitforge_core*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
- Step 3: Write package init.py
# circuitforge-core/circuitforge_core/__init__.py
__version__ = "0.1.0"
- Step 4: Write .gitignore
__pycache__/
*.pyc
.env
*.egg-info/
dist/
.pytest_cache/
- Step 5: Install editable and verify import
cd /Library/Development/CircuitForge/circuitforge-core
conda run -n job-seeker pip install -e .
conda run -n job-seeker python -c "import circuitforge_core; print(circuitforge_core.__version__)"
Expected: 0.1.0
- Step 6: Create tests/init.py
mkdir -p /Library/Development/CircuitForge/circuitforge-core/tests
touch /Library/Development/CircuitForge/circuitforge-core/tests/__init__.py
- Step 7: Commit
git add .
git commit -m "feat: scaffold circuitforge-core package"
Task 2: Extract db base connection
Files:
-
Create:
circuitforge-core/circuitforge_core/db/__init__.py -
Create:
circuitforge-core/circuitforge_core/db/base.py -
Create:
circuitforge-core/circuitforge_core/db/migrations.py -
Create:
circuitforge-core/tests/__init__.py -
Create:
circuitforge-core/tests/test_db.py -
Step 1: Write failing tests
# circuitforge-core/tests/test_db.py
import sqlite3
import tempfile
from pathlib import Path
import pytest
from circuitforge_core.db import get_connection, run_migrations
def test_get_connection_returns_sqlite_connection(tmp_path):
db = tmp_path / "test.db"
conn = get_connection(db)
assert isinstance(conn, sqlite3.Connection)
conn.close()
def test_get_connection_creates_file(tmp_path):
db = tmp_path / "test.db"
assert not db.exists()
conn = get_connection(db)
conn.close()
assert db.exists()
def test_run_migrations_applies_sql_files(tmp_path):
db = tmp_path / "test.db"
migrations_dir = tmp_path / "migrations"
migrations_dir.mkdir()
(migrations_dir / "001_create_foo.sql").write_text(
"CREATE TABLE foo (id INTEGER PRIMARY KEY, name TEXT);"
)
conn = get_connection(db)
run_migrations(conn, migrations_dir)
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='foo'")
assert cursor.fetchone() is not None
conn.close()
def test_run_migrations_is_idempotent(tmp_path):
db = tmp_path / "test.db"
migrations_dir = tmp_path / "migrations"
migrations_dir.mkdir()
(migrations_dir / "001_create_foo.sql").write_text(
"CREATE TABLE foo (id INTEGER PRIMARY KEY, name TEXT);"
)
conn = get_connection(db)
run_migrations(conn, migrations_dir)
run_migrations(conn, migrations_dir) # second run must not raise
conn.close()
def test_run_migrations_applies_in_order(tmp_path):
db = tmp_path / "test.db"
migrations_dir = tmp_path / "migrations"
migrations_dir.mkdir()
(migrations_dir / "001_create_foo.sql").write_text(
"CREATE TABLE foo (id INTEGER PRIMARY KEY);"
)
(migrations_dir / "002_add_name.sql").write_text(
"ALTER TABLE foo ADD COLUMN name TEXT;"
)
conn = get_connection(db)
run_migrations(conn, migrations_dir)
conn.execute("INSERT INTO foo (name) VALUES ('bar')")
conn.close()
- Step 2: Run tests to verify they fail
cd /Library/Development/CircuitForge/circuitforge-core
conda run -n job-seeker pytest tests/test_db.py -v
Expected: ImportError or AttributeError (module not yet defined)
- Step 3: Write db/base.py (extracted from
peregrine/scripts/db.pylines 1–40)
# circuitforge-core/circuitforge_core/db/base.py
"""
SQLite connection factory for CircuitForge products.
Supports plain SQLite and SQLCipher (AES-256) when CLOUD_MODE is active.
"""
from __future__ import annotations
import os
import sqlite3
from pathlib import Path
def get_connection(db_path: Path, key: str = "") -> sqlite3.Connection:
"""
Open a SQLite database connection.
In cloud mode with a key: uses SQLCipher (API-identical to sqlite3).
Otherwise: plain sqlite3.
Args:
db_path: Path to the database file. Created if absent.
key: SQLCipher encryption key. Empty = unencrypted.
"""
cloud_mode = os.environ.get("CLOUD_MODE", "").lower() in ("1", "true", "yes")
if cloud_mode and key:
from pysqlcipher3 import dbapi2 as _sqlcipher # type: ignore
conn = _sqlcipher.connect(str(db_path))
conn.execute(f"PRAGMA key='{key}'")
return conn
return sqlite3.connect(str(db_path))
- Step 4: Write db/migrations.py
# circuitforge-core/circuitforge_core/db/migrations.py
"""
Sequential SQL migration runner.
Applies *.sql files from migrations_dir in filename order.
Tracks applied migrations in a _migrations table — safe to call multiple times.
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
def run_migrations(conn: sqlite3.Connection, migrations_dir: Path) -> None:
"""Apply any unapplied *.sql migrations from migrations_dir."""
conn.execute(
"CREATE TABLE IF NOT EXISTS _migrations "
"(name TEXT PRIMARY KEY, applied_at TEXT DEFAULT CURRENT_TIMESTAMP)"
)
conn.commit()
applied = {row[0] for row in conn.execute("SELECT name FROM _migrations")}
sql_files = sorted(migrations_dir.glob("*.sql"))
for sql_file in sql_files:
if sql_file.name in applied:
continue
conn.executescript(sql_file.read_text())
conn.execute("INSERT INTO _migrations (name) VALUES (?)", (sql_file.name,))
conn.commit()
- Step 5: Write db/init.py
# circuitforge-core/circuitforge_core/db/__init__.py
from .base import get_connection
from .migrations import run_migrations
__all__ = ["get_connection", "run_migrations"]
- Step 6: Run tests to verify they pass
conda run -n job-seeker pytest tests/test_db.py -v
Expected: 5 PASSED
- Step 7: Commit
git add circuitforge_core/db/ tests/
git commit -m "feat: add db base connection and migration runner"
Task 3: Extract tier system
Files:
- Create:
circuitforge-core/circuitforge_core/tiers/__init__.py - Create:
circuitforge-core/circuitforge_core/tiers/tiers.py - Create:
circuitforge-core/tests/test_tiers.py
Source: peregrine/app/wizard/tiers.py
- Step 1: Write failing tests
# circuitforge-core/tests/test_tiers.py
import pytest
from circuitforge_core.tiers import can_use, TIERS, BYOK_UNLOCKABLE, LOCAL_VISION_UNLOCKABLE
def test_tiers_order():
assert TIERS == ["free", "paid", "premium", "ultra"]
def test_free_feature_always_accessible():
# Features not in FEATURES dict are free for everyone
assert can_use("nonexistent_feature", tier="free") is True
def test_paid_feature_blocked_for_free_tier():
# Caller must register features — test via can_use with explicit min_tier
assert can_use("test_paid", tier="free", _features={"test_paid": "paid"}) is False
def test_paid_feature_accessible_for_paid_tier():
assert can_use("test_paid", tier="paid", _features={"test_paid": "paid"}) is True
def test_byok_unlocks_byok_feature():
byok_feature = next(iter(BYOK_UNLOCKABLE)) if BYOK_UNLOCKABLE else None
if byok_feature:
assert can_use(byok_feature, tier="free", has_byok=True) is True
def test_byok_does_not_unlock_non_byok_feature():
assert can_use("test_paid", tier="free", has_byok=True,
_features={"test_paid": "paid"}) is False
def test_local_vision_unlocks_vision_feature():
vision_feature = next(iter(LOCAL_VISION_UNLOCKABLE)) if LOCAL_VISION_UNLOCKABLE else None
if vision_feature:
assert can_use(vision_feature, tier="free", has_local_vision=True) is True
def test_local_vision_does_not_unlock_non_vision_feature():
assert can_use("test_paid", tier="free", has_local_vision=True,
_features={"test_paid": "paid"}) is False
- Step 2: Run tests to verify they fail
conda run -n job-seeker pytest tests/test_tiers.py -v
Expected: ImportError
- Step 3: Write tiers/tiers.py (generalised from
peregrine/app/wizard/tiers.py— remove all Peregrine-specificFEATURESentries; keep thecan_uselogic and unlock mechanism)
# circuitforge-core/circuitforge_core/tiers/tiers.py
"""
Tier system for CircuitForge products.
Tiers: free < paid < premium < ultra
Products register their own FEATURES dict and pass it to can_use().
BYOK_UNLOCKABLE: features that unlock when the user has any configured
LLM backend (local or API key). These are gated only because CF would
otherwise provide the compute.
LOCAL_VISION_UNLOCKABLE: features that unlock when the user has a local
vision model configured (e.g. moondream2). Distinct from BYOK — a text
LLM key does NOT unlock vision features.
"""
from __future__ import annotations
TIERS: list[str] = ["free", "paid", "premium", "ultra"]
# Features that unlock when the user has any LLM backend configured.
# Each product extends this frozenset with its own BYOK-unlockable features.
BYOK_UNLOCKABLE: frozenset[str] = frozenset()
# Features that unlock when the user has a local vision model configured.
LOCAL_VISION_UNLOCKABLE: frozenset[str] = frozenset()
def can_use(
feature: str,
tier: str,
has_byok: bool = False,
has_local_vision: bool = False,
_features: dict[str, str] | None = None,
) -> bool:
"""
Return True if the given tier (and optional unlocks) can access feature.
Args:
feature: Feature key string.
tier: User's current tier ("free", "paid", "premium", "ultra").
has_byok: True if user has a configured LLM backend.
has_local_vision: True if user has a local vision model configured.
_features: Feature→min_tier map. Products pass their own dict here.
If None, all features are free.
"""
features = _features or {}
if feature not in features:
return True
if has_byok and feature in BYOK_UNLOCKABLE:
return True
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
return True
min_tier = features[feature]
try:
return TIERS.index(tier) >= TIERS.index(min_tier)
except ValueError:
return False
def tier_label(
feature: str,
has_byok: bool = False,
has_local_vision: bool = False,
_features: dict[str, str] | None = None,
) -> str:
"""Return a human-readable label for the minimum tier needed for feature."""
features = _features or {}
if feature not in features:
return "free"
if has_byok and feature in BYOK_UNLOCKABLE:
return "free (BYOK)"
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
return "free (local vision)"
return features[feature]
- Step 4: Write tiers/init.py
# circuitforge-core/circuitforge_core/tiers/__init__.py
from .tiers import can_use, tier_label, TIERS, BYOK_UNLOCKABLE, LOCAL_VISION_UNLOCKABLE
__all__ = ["can_use", "tier_label", "TIERS", "BYOK_UNLOCKABLE", "LOCAL_VISION_UNLOCKABLE"]
- Step 5: Run tests
conda run -n job-seeker pytest tests/test_tiers.py -v
Expected: 8 PASSED
- Step 6: Commit
git add circuitforge_core/tiers/ tests/test_tiers.py
git commit -m "feat: add generalised tier system with BYOK and local vision unlocks"
Task 4: Extract LLM router
Files:
- Create:
circuitforge-core/circuitforge_core/llm/__init__.py - Create:
circuitforge-core/circuitforge_core/llm/router.py - Create:
circuitforge-core/tests/test_llm_router.py
Source: peregrine/scripts/llm_router.py
- Step 1: Write failing tests
# circuitforge-core/tests/test_llm_router.py
from unittest.mock import MagicMock, patch
import pytest
from circuitforge_core.llm import LLMRouter
def _make_router(config: dict) -> LLMRouter:
"""Build a router from an in-memory config dict (bypass file loading)."""
router = object.__new__(LLMRouter)
router.config = config
return router
def test_complete_uses_first_reachable_backend():
router = _make_router({
"fallback_order": ["local"],
"backends": {
"local": {
"type": "openai_compat",
"base_url": "http://localhost:11434/v1",
"model": "llama3",
"supports_images": False,
}
}
})
mock_client = MagicMock()
mock_client.chat.completions.create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="hello"))]
)
with patch.object(router, "_is_reachable", return_value=True), \
patch("circuitforge_core.llm.router.OpenAI", return_value=mock_client):
result = router.complete("say hello")
assert result == "hello"
def test_complete_falls_back_on_unreachable_backend():
router = _make_router({
"fallback_order": ["unreachable", "working"],
"backends": {
"unreachable": {
"type": "openai_compat",
"base_url": "http://nowhere:1/v1",
"model": "x",
"supports_images": False,
},
"working": {
"type": "openai_compat",
"base_url": "http://localhost:11434/v1",
"model": "llama3",
"supports_images": False,
}
}
})
mock_client = MagicMock()
mock_client.chat.completions.create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="fallback"))]
)
def reachable(url):
return "nowhere" not in url
with patch.object(router, "_is_reachable", side_effect=reachable), \
patch("circuitforge_core.llm.router.OpenAI", return_value=mock_client):
result = router.complete("test")
assert result == "fallback"
def test_complete_raises_when_all_backends_exhausted():
router = _make_router({
"fallback_order": ["dead"],
"backends": {
"dead": {
"type": "openai_compat",
"base_url": "http://nowhere:1/v1",
"model": "x",
"supports_images": False,
}
}
})
with patch.object(router, "_is_reachable", return_value=False):
with pytest.raises(RuntimeError, match="exhausted"):
router.complete("test")
- Step 2: Run tests to verify they fail
conda run -n job-seeker pytest tests/test_llm_router.py -v
Expected: ImportError
- Step 3: Copy LLM router from Peregrine
Copy the full contents of /Library/Development/CircuitForge/peregrine/scripts/llm_router.py to circuitforge-core/circuitforge_core/llm/router.py, then update the one internal import:
# Change at top of file:
# OLD: CONFIG_PATH = Path(__file__).parent.parent / "config" / "llm.yaml"
# NEW:
CONFIG_PATH = Path.home() / ".config" / "circuitforge" / "llm.yaml"
- Step 4: Write llm/init.py
# circuitforge-core/circuitforge_core/llm/__init__.py
from .router import LLMRouter
__all__ = ["LLMRouter"]
- Step 5: Run tests
conda run -n job-seeker pytest tests/test_llm_router.py -v
Expected: 3 PASSED
- Step 6: Commit
git add circuitforge_core/llm/ tests/test_llm_router.py
git commit -m "feat: add LLM router (extracted from Peregrine)"
Task 5: Add vision stub and config module
Files:
-
Create:
circuitforge-core/circuitforge_core/vision/__init__.py -
Create:
circuitforge-core/circuitforge_core/vision/router.py -
Create:
circuitforge-core/circuitforge_core/config/__init__.py -
Create:
circuitforge-core/circuitforge_core/config/settings.py -
Create:
circuitforge-core/tests/test_config.py -
Step 1: Write failing config tests
# circuitforge-core/tests/test_config.py
import os
import pytest
from circuitforge_core.config import require_env, load_env
def test_require_env_returns_value_when_set(monkeypatch):
monkeypatch.setenv("TEST_KEY", "hello")
assert require_env("TEST_KEY") == "hello"
def test_require_env_raises_when_missing(monkeypatch):
monkeypatch.delenv("TEST_KEY", raising=False)
with pytest.raises(EnvironmentError, match="TEST_KEY"):
require_env("TEST_KEY")
def test_load_env_sets_variables(tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text("FOO=bar\nBAZ=qux\n")
monkeypatch.delenv("FOO", raising=False)
load_env(env_file)
assert os.environ.get("FOO") == "bar"
assert os.environ.get("BAZ") == "qux"
def test_load_env_skips_missing_file(tmp_path):
load_env(tmp_path / "nonexistent.env") # must not raise
- Step 2: Run to verify failure
conda run -n job-seeker pytest tests/test_config.py -v
Expected: ImportError
- Step 3: Write config/settings.py
# circuitforge-core/circuitforge_core/config/settings.py
"""Env validation and .env loader for CircuitForge products."""
from __future__ import annotations
import os
from pathlib import Path
def require_env(key: str) -> str:
"""Return env var value or raise EnvironmentError with clear message."""
value = os.environ.get(key)
if not value:
raise EnvironmentError(
f"Required environment variable {key!r} is not set. "
f"Check your .env file."
)
return value
def load_env(path: Path) -> None:
"""Load key=value pairs from a .env file into os.environ. Skips missing files."""
if not path.exists():
return
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
- Step 4: Write config/init.py
# circuitforge-core/circuitforge_core/config/__init__.py
from .settings import require_env, load_env
__all__ = ["require_env", "load_env"]
- Step 5: Write vision stub
# circuitforge-core/circuitforge_core/vision/router.py
"""
Vision model router — stub until v0.2.
Supports: moondream2 (local) and Claude vision API (cloud).
"""
from __future__ import annotations
class VisionRouter:
"""Routes image analysis requests to local or cloud vision models."""
def analyze(self, image_bytes: bytes, prompt: str) -> str:
"""
Analyze image_bytes with the given prompt.
Raises NotImplementedError until vision backends are wired up.
"""
raise NotImplementedError(
"VisionRouter is not yet implemented. "
"Photo analysis requires a Paid tier or local vision model (v0.2+)."
)
# circuitforge-core/circuitforge_core/vision/__init__.py
from .router import VisionRouter
__all__ = ["VisionRouter"]
- Step 6: Run all tests
conda run -n job-seeker pytest tests/ -v
Expected: All PASSED (config tests pass; vision stub has no tests — it's a placeholder)
- Step 7: Commit
git add circuitforge_core/vision/ circuitforge_core/config/ tests/test_config.py
git commit -m "feat: add config module and vision router stub"
Task 5b: Add wizard and pipeline stubs
Files:
- Create:
circuitforge-core/circuitforge_core/wizard/__init__.py - Create:
circuitforge-core/circuitforge_core/wizard/base.py - Create:
circuitforge-core/circuitforge_core/pipeline/__init__.py - Create:
circuitforge-core/circuitforge_core/pipeline/staging.py - Create:
circuitforge-core/tests/test_stubs.py
These modules are required by the spec (section 2.2) but are net-new (wizard) or partially net-new (pipeline). They are stubbed here so downstream products can import them; full implementation happens when each product needs them.
- Step 1: Write failing stub import tests
# circuitforge-core/tests/test_stubs.py
import pytest
from circuitforge_core.wizard import BaseWizard
from circuitforge_core.pipeline import StagingDB
def test_wizard_raises_not_implemented():
wizard = BaseWizard()
with pytest.raises(NotImplementedError):
wizard.run()
def test_pipeline_raises_not_implemented():
staging = StagingDB()
with pytest.raises(NotImplementedError):
staging.enqueue("job", {})
- Step 2: Run to verify failure
conda run -n job-seeker pytest tests/test_stubs.py -v
Expected: ImportError
- Step 3: Write wizard stub
# circuitforge-core/circuitforge_core/wizard/base.py
"""
First-run onboarding wizard base class.
Full implementation is net-new per product (v0.1+ for snipe, etc.)
"""
from __future__ import annotations
class BaseWizard:
"""
Base class for CircuitForge first-run wizards.
Subclass and implement run() in each product.
"""
def run(self) -> None:
"""Execute the onboarding wizard flow. Must be overridden by subclass."""
raise NotImplementedError(
"BaseWizard.run() must be implemented by a product-specific subclass."
)
# circuitforge-core/circuitforge_core/wizard/__init__.py
from .base import BaseWizard
__all__ = ["BaseWizard"]
- Step 4: Write pipeline stub
# circuitforge-core/circuitforge_core/pipeline/staging.py
"""
SQLite-backed staging queue for CircuitForge pipeline tasks.
Full implementation deferred — stub raises NotImplementedError.
"""
from __future__ import annotations
from typing import Any
class StagingDB:
"""
Staging queue for background pipeline tasks (search polling, score updates, etc.)
Stub: raises NotImplementedError until wired up in a product.
"""
def enqueue(self, task_type: str, payload: dict[str, Any]) -> None:
"""Add a task to the staging queue."""
raise NotImplementedError(
"StagingDB.enqueue() is not yet implemented. "
"Background task pipeline is a v0.2+ feature."
)
def dequeue(self) -> tuple[str, dict[str, Any]] | None:
"""Fetch the next pending task. Returns (task_type, payload) or None."""
raise NotImplementedError(
"StagingDB.dequeue() is not yet implemented."
)
# circuitforge-core/circuitforge_core/pipeline/__init__.py
from .staging import StagingDB
__all__ = ["StagingDB"]
- Step 5: Run tests
conda run -n job-seeker pytest tests/test_stubs.py -v
Expected: 2 PASSED
- Step 6: Verify all tests still pass
conda run -n job-seeker pytest tests/ -v
Expected: All PASSED
- Step 7: Commit
git add circuitforge_core/wizard/ circuitforge_core/pipeline/ tests/test_stubs.py
git commit -m "feat: add wizard and pipeline stubs"
Task 6: Update Peregrine to use circuitforge-core
Files:
-
Modify:
peregrine/requirements.txt -
Modify:
peregrine/scripts/db.py -
Modify:
peregrine/scripts/llm_router.py -
Modify:
peregrine/app/wizard/tiers.py -
Step 1: Add circuitforge-core to Peregrine requirements
Add to /Library/Development/CircuitForge/peregrine/requirements.txt (top of file, before other deps):
-e ../circuitforge-core
- Step 2: Verify install
cd /Library/Development/CircuitForge/peregrine
conda run -n job-seeker pip install -r requirements.txt
conda run -n job-seeker python -c "from circuitforge_core.db import get_connection; print('ok')"
Expected: ok
- Step 3: Update peregrine/scripts/db.py — replace get_connection
Add at top of file (after existing docstring and imports):
from circuitforge_core.db import get_connection as _cf_get_connection
def get_connection(db_path=DEFAULT_DB, key=""):
"""Thin shim — delegates to circuitforge_core.db.get_connection."""
return _cf_get_connection(db_path, key)
Remove the old get_connection function body (lines ~15–35). Keep all Peregrine-specific schema (CREATE_JOBS, CREATE_COVER_LETTERS, etc.) and functions unchanged.
- Step 4: Run Peregrine tests to verify nothing broke
cd /Library/Development/CircuitForge/peregrine
conda run -n job-seeker pytest tests/ -v -x
Expected: All existing tests pass
- Step 5: Update peregrine/scripts/llm_router.py — replace LLMRouter
Replace the LLMRouter class definition with:
from circuitforge_core.llm import LLMRouter # noqa: F401 — re-export for existing callers
Keep the module-level CONFIG_PATH constant so any code that references llm_router.CONFIG_PATH still works.
- Step 6: Run Peregrine tests again
conda run -n job-seeker pytest tests/ -v -x
Expected: All passing
- Step 7: Update peregrine/app/wizard/tiers.py — import from core
At the top of the file, add:
from circuitforge_core.tiers import can_use as _core_can_use, TIERS, tier_label
Update the can_use function to delegate to core, passing Peregrine's FEATURES dict:
def can_use(feature: str, tier: str, has_byok: bool = False, has_local_vision: bool = False) -> bool:
return _core_can_use(feature, tier, has_byok=has_byok, has_local_vision=has_local_vision, _features=FEATURES)
Keep the Peregrine-specific FEATURES dict and BYOK_UNLOCKABLE frozenset in place — they are still defined here, just used via the core function.
- Step 8: Run full Peregrine tests
conda run -n job-seeker pytest tests/ -v
Expected: All passing
- Step 9: Smoke-test Peregrine startup
cd /Library/Development/CircuitForge/peregrine
./manage.sh status # verify it's running, or start it
Manually open http://localhost:8502 and verify the UI loads without errors.
- Step 10: Commit Peregrine changes
cd /Library/Development/CircuitForge/peregrine
git add requirements.txt scripts/db.py scripts/llm_router.py app/wizard/tiers.py
git commit -m "feat: migrate to circuitforge-core for db, llm router, and tiers"
- Step 11: Commit circuitforge-core README and push both repos
cd /Library/Development/CircuitForge/circuitforge-core
# Write a minimal README then:
git add README.md
git commit -m "docs: add README"
git remote add origin https://git.opensourcesolarpunk.com/Circuit-Forge/circuitforge-core.git
git push -u origin main
Task 7: Final verification
- Step 1: Run circuitforge-core full test suite
cd /Library/Development/CircuitForge/circuitforge-core
conda run -n job-seeker pytest tests/ -v --tb=short
Expected: All PASSED
- Step 2: Run Peregrine full test suite
cd /Library/Development/CircuitForge/peregrine
conda run -n job-seeker pytest tests/ -v --tb=short
Expected: All PASSED
- Step 3: Verify editable install works from a fresh shell
conda run -n job-seeker python -c "
from circuitforge_core.db import get_connection, run_migrations
from circuitforge_core.llm import LLMRouter
from circuitforge_core.tiers import can_use, TIERS
from circuitforge_core.config import require_env, load_env
from circuitforge_core.vision import VisionRouter
from circuitforge_core.wizard import BaseWizard
from circuitforge_core.pipeline import StagingDB
print('All imports OK')
"
Expected: All imports OK