# Agent: Foundation (config, base, helpers, runner)

## Your Task

Build the shared foundation for the analyzer framework in `analysis-14022026/analyzers/`. All other agents depend on your output.

## Files to Create

### 1. `analysis-14022026/analyzers/__init__.py`
Empty init.

### 2. `analysis-14022026/analyzers/config.py`
- Reuse the pattern from `analysis-14022026/utils/connection.py`
- `get_connection(env="local")` — env="local" uses `.local.env`, env="prod" uses `.prod.env`
- Keep the PROD blocker safety check from `utils/connection.py`

### 3. `analysis-14022026/analyzers/helpers.py`

Shared constants and utils needed by ALL analyzers. Source of truth: `analysis-14022026/session_view_scripts/research/ideation.md`

**Tool name sets:**
- `SHELL_TOOLS = {"Bash", "shell_command", "exec_command", "run_shell_command", "run_terminal_cmd"}`
- `EDIT_TOOLS = {"Edit", "Write", "MultiEdit", "MultiEditTool", "replace"}`
- `EXPLORE_TOOLS = {"Read", "Grep", "Glob", "read_file", "grep_search", "codebase_search", "list_dir", "file_search"}`
- `PLAN_TOOLS = {"TodoWrite", "TodoRead", "TaskCreate", "TaskUpdate", "ExitPlanMode"}`
- `DELEGATION_TOOLS = {"Task", "TaskOutput"}`
- `WEB_TOOLS = {"WebSearch", "WebFetch"}`

**`categorize_tool(tool_name: str) -> str`** — returns "shell", "edit", "explore", "plan", "delegation", "web", "mcp" (if starts with `mcp__`), or "other"

**`clean_user_content(content: str, source: str) -> str`** — strips source-specific noise:
- Codex CLI: remove leading `<INSTRUCTIONS>...</INSTRUCTIONS>`, `<environment_context>...</environment_context>`, `# AGENTS.md` blocks (first 1-3 user messages are system XML)
- Gemini CLI: truncate at `--- Content from referenced files ---`
- Claude Code: remove `<command-name>...</command-name>` slash command messages
- All: skip messages starting with `This session is being continued from a previous conversation` (compaction)

**`is_compaction_message(content: str) -> bool`**

**`detect_interrupt_pattern(tool_result_output: str) -> dict`** — parse the 3 interrupt patterns from `analysis-14022026/session_view_scripts/research/06_interrupt_output_taxonomy.py`:
- INLINE GUIDANCE: output contains `"the user said:"` → extract text after it
- REJECTED ONLY: output contains `"was rejected"` but no inline guidance
- STOP ONLY: output contains `"STOP what you are doing"`
- Returns `{"pattern": "inline"|"rejected"|"stop", "guidance_text": str|None}`

**`is_shell_error(output: str) -> bool`** — check tool_result.output for error indicators: `exit code` > 0, `Error`, `FAILED`, `error:`, `command not found`, `No such file`

### 4. `analysis-14022026/analyzers/base.py`

**`load_session_data(conn, email: str, since: str, until: str) -> dict`**

Queries DB once, returns dict with:
- `sessions`: DataFrame from `SELECT * FROM sessions WHERE user_email = %s AND first_seen >= %s AND first_seen < %s`
- `messages`: DataFrame from messages table joined for all session_ids, ordered by session_id + timestamp
- `tool_calls`: DataFrame from tool_calls for all message_ids
- `tool_results`: DataFrame from tool_results for all message_ids
- `token_usage`: DataFrame from token_usage for all message_ids

Reuse query patterns from `analysis-14022026/utils/sessions.py`, `messages.py`, `tools.py`, `tokens.py`.

**`class BaseAnalyzer`**:
```python
class BaseAnalyzer:
    name: str

    def analyze(self, sessions_df, messages_df, tool_calls_df, tool_results_df, token_usage_df) -> dict:
        raise NotImplementedError

    def get_session_messages(self, messages_df, session_id) -> pd.DataFrame:
        return messages_df[messages_df["session_id"] == session_id]
```

### 5. `analysis-14022026/analyzers/runner.py`

CLI entry point:
```bash
python -m analysis-14022026.analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14
python -m analysis-14022026.analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14 --analyzers a01,a02
python -m analysis-14022026.analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14 --env prod
python -m analysis-14022026.analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14 --include-llm
```

- Uses argparse
- Loads data once via `load_session_data()`
- Discovers and runs selected analyzers
- Writes each result to `analysis-14022026/analyzers/output/{email}/{since}_to_{until}/{analyzer_name}.json`
- Writes combined `all.json` with all results merged
- Prints summary: sessions found, analyzers run, output path

## Key Existing Code to Read First

1. `analysis-14022026/utils/connection.py` — connection pattern to copy
2. `analysis-14022026/utils/sessions.py` — query patterns for sessions by user + date range
3. `analysis-14022026/utils/messages.py` — message queries
4. `analysis-14022026/utils/tools.py` — tool_calls and tool_results queries
5. `analysis-14022026/utils/tokens.py` — token usage queries
6. `analysis-14022026/utils/pricing.py` — `calc_cost()` for cost calculations. DO NOT reimplement cost logic, import and use this.
7. `analysis-14022026/session_view_scripts/research/06_interrupt_output_taxonomy.py` — interrupt pattern detection logic
8. `analysis-14022026/session_view_scripts/research/ideation.md` — full feature spec (sections on data quirks, tool names, detection patterns)

## Verification

1. `docker compose -f analysis-14022026/docker-compose.yml up -d` (if not already running)
2. Run: `python -m analysis-14022026.analyzers.runner --email <any_email_from_db> --since 2025-01-01 --until 2025-02-15`
3. Should print: X sessions found, 0 analyzers run (no analyzers implemented yet), data loaded successfully
4. Check output dir was created

## Commit Convention

Use conventional commits: `feat:`, `fix:`, etc. No Claude/Anthropic attribution.
