# Swival Project Conventions

Use uv to run python.

## Error Handling & Return Values
- Tools always return strings, never exceptions. Errors start with `"error:"` prefix for detection via `result.startswith("error:")`.
- State objects (`think`, `todo`, `snapshot`) return JSON on success; parse with `json.loads()`.
- Exceptions (`AgentError`, `ConfigError`) are raised only at system boundaries, not in tool implementations.

## Message Handling
- Always use message accessor helpers from `_msg.py` — never index messages directly because they can be `dict` or namespace objects.
- Messages are mutated in place; compaction uses `messages[:] = ...` to preserve the reference for REPL mode.

## Configuration
- Precedence: CLI flags > project `swival.toml` > global config > hardcoded defaults.
- The `_UNSET` sentinel distinguishes "not provided" from `False`/`None`; only appears in argparse layer, never downstream.
- Key inversions apply: `no_read_guard → read_guard`, `no_history → history`, `quiet → verbose`.

## State Objects Pattern
- `ThinkingState`, `TodoState`, `SnapshotState`, and `FileAccessTracker` all have `reset()` (for `/clear`) and `summary_line()` methods.
- All accept `verbose: bool = False` in constructor; state objects share `.process(args) -> str` dispatch interface.

## Output Channels & Exit Codes
- stdout is exclusively the final answer; all diagnostics go to stderr via Rich Console (`stderr=True`).
- Exit codes: `0` = success, `1` = error during setup, `2` = exhausted (max turns without answer).

## File Safety Pattern
- `.swival/` holds runtime state: `HISTORY.md`, `todo.md`, `repl_history`, `trash/`, `cmd_output_*.txt`.
- Path safety uses `_safe_*_path()` helpers that call `.resolve()` and `is_relative_to(base)` to block symlink escapes.

## Edit Tool Conventions
- Three-pass matching: exact → line-trimmed (per-line `.strip()`) → unicode-normalized (smart quotes/dashes/ellipsis → ASCII).
- Raises `ValueError` with messages `"not found"`, `"multiple matches"`, `"no changes"`, or `"old_string must not be empty"`; caught and converted to `"error: ..."` strings.

## MCP Integration
- Tool namespacing: `mcp__{server_name}__{sanitized_tool_name}`; original name stored as `_mcp_original_name` in schema dict.
- `McpManager` runs background asyncio event loop in daemon thread; all methods are synchronous via `run_coroutine_threadsafe()` — never use `asyncio.run()`.
- Servers marked degraded after any call failure; subsequent calls return error immediately.

## Provider String Formats (LiteLLM)
- lmstudio: `openai/{model_id}` with `api_base = "{base_url}/v1"`, `api_key = "lm-studio"`
- generic: `openai/{model_id}` with `/v1` auto-appended if missing; openrouter: `openrouter/{model_id}`; huggingface: must include `org/`.

## Context Compaction Pipeline
On `ContextOverflowError`, runs in order: 1) `compact_messages()` (shrinks tool results, keeps last 2), 2) `drop_middle_turns()` (scoring: errors +3, write/edit +5, think +2, snapshot recap +5), 3) `aggressive_drop_turns()` (keeps only system + last 2).
- `enforce_mcp_token_budget()` drops entire MCP servers if tool schemas exceed 50% of context.

## Behavioral Nudges (Injected as User Messages)
- Tool-error guardrail: 2 repeats → IMPORTANT warning, 3+ → STOP; tracked per canonical error in `consecutive_errors` dict.
- Think nudge fires once if first write/edit happens without prior `think`; todo reminder every 3 turns if items remain; snapshot nudge after 5 consecutive read-only turns.

## Public API (`__init__.py`)
- Exports: `swival.run()`, `Session`, `Result`, `AgentError`, `ConfigError`.
- `Session.run()` is single-shot (fresh state); `Session.ask()` is multi-turn (shared state); `Session.reset()` clears only conversation state.
- `Session` is a context manager; always use `with Session(...) as s:` when MCP servers are configured.
