This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
All commands use just (task runner):
just check— run all checks (ruff lint, ruff format, mypy, license headers)just test— run all tests with coveragejust test tests/infra/test_service.py— single test filejust test tests/infra/test_service.py::test_function_name— single testjust ruff— lintjust ruff-format— formatjust mypy— type checkjust license-check— verify license headersjust df <args>— run the CLI locally (e.g.just df infra --help)
- Ruff formatter: 120 char line length, 4-space indent
- Type hints required on all function parameters and return values
- Google-style docstrings on all public functions
- isort import ordering: future → stdlib → third-party → first-party → local
- All Python files must include the DeepFellow Free License header (enforced by
scripts/check_license_header.py)
- Use plain pytest functions — no TestClass
- Test file names:
test_<module>.py(e.g.,tests/infra/test_service.py) - Directory structure mirrors source:
tests/common/,tests/infra/,tests/server/ - Test names include function name + reason:
test_echoblock_run_success,test_list_workflows_returns_empty_list - Use Arrange, Act, Assert (AAA) pattern in function body
- Use
@mock.patchdecorators, notwith mock.patchcontext managers - No "magic" mock asserts — use
assert mocked.call_count == 1andassert mocked.call_args == mock.call(...)instead ofmocked.assert_called_once_with(...) - Directory-scoped
conftest.pyfor fixtures used only within one test directory
Three Typer command groups under deepfellow/, all registered in main.py:
deepfellow/infra/— infrastructure management (Docker Compose-based infra stack)deepfellow/server/— server management (DeepFellow Server API + Docker)deepfellow/cli/— CLI self-management (e.g.,cli update)deepfellow/common/— shared utilities
Each final subcommand lives in its own file; wired in the group's __init__.py. See @AGENTS.md for details.
Subgroups (nested command groups within infra/ and server/) use name= in add_typer():
infra env,infra service,infra modelserver env,server organization,server project
All output and prompting goes through the echo singleton (a rich.Console subclass):
from deepfellow.common.echo import echo
echo.info(msg) # informational
echo.success(msg) # green ✅
echo.warning(msg) # yellow ⚠️
echo.error(msg) # red 💀
echo.debug(msg) # only when state.debug=True
echo.confirm(msg) # bool prompt; respects --non-interactive
echo.prompt(...) # string prompt with optional validation
echo.prompt_until_valid(...) # retries until validation passes
echo.choice(...) # questionary selectInteractive mode (default) shows emojis and rich formatting. Non-interactive mode (--non-interactive) strips formatting and uses defaults or raises typer.Exit(1) if no default.
Both files use .env format and live at ~/.deepfellow/. Paths are in state.cli_config_file / state.cli_secrets_file.
- Config (
~/.deepfellow/config): CLI preferences, e.g.df_server_url - Secrets (
~/.deepfellow/secrets):DF_USER_TOKEN,DF_USER_REFRESH_TOKEN
Environment variable keys use DF_ prefix and __ as nesting separator (e.g. DF_VECTOR_DATABASE__PROVIDER__TYPE). config.py provides read_env_file, save_env_file, env_to_dict, and dict_to_env for round-tripping.
REST helpers handle auth headers and common HTTP error codes centrally:
from deepfellow.common import rest
server = rest.get_server_url(server_arg) # resolves, saves, health-checks
data = rest.get(url, token)
data = rest.post(url, token, data={...})
rest.delete(url, token)
rest.make_request(method, url, token, data={...})Server commands get a valid bearer token via get_token(state.cli_secrets_file, server) from deepfellow/server/utils/login.py — handles access token validation and refresh automatically.
Use run() for all subprocess calls — it strips VIRTUAL_ENV, handles debug reraise, and returns stdout or None on failure:
from deepfellow.common.system import run
result = run(["docker", "ps"], capture_output=True) # returns stdout or None
run(["docker", "compose", "up", "-d"], cwd=directory) # raises on failure in debug mode
run(["cmd"], raises=MyError) # raises MyError on failureInfra and server commands build Docker Compose configs programmatically then persist them:
from deepfellow.common.docker import load_compose_file, merge_services, save_compose_file
compose = load_compose_file(directory / "compose.yaml") # dict or {"services": {}}
merged = merge_services(DOCKER_COMPOSE_SERVER, DOCKER_COMPOSE_QDRANT) # combine service dicts
save_compose_file(merged, directory / "compose.yaml") # writes YAMLAll default Docker Compose service templates and constants (ports, images, env vars) live in deepfellow/common/defaults.py.
The infra env and server env command groups use shared utilities:
from deepfellow.common.env import env_get, env_set, print_env_info, EnvMetadata
env_set(env_file, "DF_SOME_KEY", value) # writes to .env file; adds DF_ prefix if missing
value = env_get(env_file, "DF_SOME_KEY") # reads from .env fileprint_env_info(header, env_metadata, env_values) renders a formatted info/doc view. EnvMetadata(description, sensitive) marks fields for display — sensitive values show as ***** unless show_secret=True.
All infra commands that operate on a local install share a standard --directory option:
from deepfellow.infra.utils.options import directory_option
def my_command(directory: Path = directory_option(exists=True)) -> None: ...exists=True enables Typer path validation (readable, writable, must exist). Default is ~/.deepfellow/infra.
from deepfellow.common.exceptions import reraise_if_debug
try:
...
except SomeError as exc:
echo.error("Human-readable message")
reraise_if_debug(exc) # re-raises in debug mode, otherwise typer.Exit(1)CLI-wide flags set by main() are stored in a module-level AppState singleton:
from deepfellow.common.state import state
state.debug # bool — --debug / --verbose / -v / -vv flag
state.yes # bool — --yes / -y flag
state.non_interactive # bool — --non-interactive flag
state.cli_config # dict — parsed CLI config file
state.cli_config_file # Path — path to CLI config file
state.cli_secrets_file # Path — path to CLI secrets filemain() populates state once at startup. Every module — including all final subcommands — reads CLI flags from state directly. Subcommands no longer take a ctx: typer.Context parameter for this. Never use click.get_current_context().
- GitLab-hosted; use
glabCLI for branches and MRs - Never merge MRs via CLI — merging is done manually in GitLab UI
- Never add
Co-Authored-Bylines to commit messages - Never include
openspec/or.opencode/files in git commits