# Plan: Parallel Feature Engineering Agents for Session Analysis

## Context

The ideation doc (`analysis-14022026/session_view_scripts/research/ideation.md`) defines 13 feature groups (A1-A10, B1-B4, C1-C4) for analyzing AI coding sessions. We need independent, parallelizable analysis scripts that:
- Take **email** + **date range** as input
- Query **local Docker PostgreSQL** (configurable to prod later)
- Output **JSON** per analysis
- Run in **separate git worktrees** for parallel development

## Architecture

### Shared Foundation

**`analysis-14022026/analyzers/`** — new directory for all analyzer code.

```
analyzers/
├── config.py          # DB config (env toggle: .local.env / .prod.env)
├── base.py            # BaseAnalyzer class + shared data loading
├── runner.py          # CLI entry: python -m analyzers.runner --email X --since Y --until Z --analyzers A1,A2
├── helpers.py         # Shared: source-noise stripping, tool categorization, shell tool names
│
├── a01_interruptions.py
├── a02_autonomy.py
├── a03_prompt_signals.py
├── a04_session_outcomes.py
├── a05_undo_revert.py
├── a06_ai_clarification.py
├── a07_conversation_dynamics.py
├── a08_session_linkage.py
├── a09_tool_profile.py
├── a10_error_detection.py
│
├── b01_task_classification.py   # LLM-based (OpenAI)
├── b03_prompt_quality.py        # LLM-based
├── b04_failure_root_cause.py    # LLM-based
│
├── c01_developer_profile.py     # Cross-session aggregation
└── c02_team_trends.py           # Cross-session aggregation
```

### BaseAnalyzer Pattern

```python
class BaseAnalyzer:
    """Each analyzer implements this interface."""
    name: str  # e.g. "a01_interruptions"

    def analyze(self, sessions_df, messages, tool_calls, tool_results) -> dict:
        """Return JSON-serializable dict of results."""
        ...
```

### Data Loading (base.py)

Single shared loader that queries the DB once and passes data to all analyzers:

1. **sessions** — `SELECT * FROM sessions WHERE user_email = %s AND first_seen BETWEEN %s AND %s`
2. **messages** — `SELECT * FROM messages WHERE session_id = ANY(%s) ORDER BY timestamp`
3. **tool_calls** — `SELECT * FROM tool_calls WHERE message_id = ANY(%s)`
4. **tool_results** — `SELECT * FROM tool_results WHERE message_id = ANY(%s)`

All returned as DataFrames. Each analyzer gets the same data — no redundant queries.

### Config (config.py)

```python
def get_connection(env: str = "local") -> psycopg.Connection:
    """env: 'local' (.local.env) or 'prod' (.prod.env)"""
```

Reuses existing `analysis-14022026/utils/connection.py` pattern. Default = local Docker (`localhost:15432`).

### Runner CLI

```bash
# Run all heuristic analyzers for a user
python -m analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14

# Run specific analyzers
python -m analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14 --analyzers a01,a02,a04

# Use prod DB
python -m analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14 --env prod

# Run LLM analyzers too
python -m analyzers.runner --email user@example.com --since 2025-01-01 --until 2025-02-14 --include-llm
```

Output: `analyzers/output/{email}/{date_range}/` with one JSON per analyzer + a combined `all.json`.

### helpers.py — Shared Constants & Utils

From ideation.md, these are needed by multiple analyzers:

- **Source noise stripping**: Codex XML prefixes, Gemini file refs, compaction text, slash commands
- **Shell tool names**: `Bash`, `shell_command`, `exec_command`, `run_shell_command`, `run_terminal_cmd`
- **Edit tool names**: `Edit`, `Write`, `MultiEdit`, `MultiEditTool`, `replace`
- **Explore tool names**: `Read`, `Grep`, `Glob`, `read_file`, `grep_search`, `codebase_search`, `list_dir`, `file_search`
- **Plan tool names**: `TodoWrite`, `TodoRead`, `TaskCreate`, `TaskUpdate`, `ExitPlanMode`
- **Tool category mapper**: tool_name → category string
- **Interrupt detection**: parse `tool_result.output` for the 3 patterns (inline guidance, rejected only, stop only)

## Analyzer Specs (what each outputs)

### A1: Interruptions → `a01_interruptions.json`
```json
{
  "total_interrupts": 12,
  "sessions_with_interrupts": 5,
  "per_session": [{
    "session_id": "...",
    "interrupt_count": 3,
    "first_interrupt_position_pct": 22.5,
    "interrupt_position_bucket": "mostly_early",
    "interrupts": [{
      "tool_name": "Bash",
      "tool_category": "shell",
      "position_pct": 22.5,
      "guidance_source": "inline",
      "guidance_text": "no we don't have to change anything here",
      "time_to_next_msg_sec": 14.2
    }],
    "session_recovered": true
  }]
}
```

### A2: Autonomy → `a02_autonomy.json`
Per-session: `tool_calls_per_user_msg`, `max_tool_streak`, `avg_tool_streak`, `autonomous_segments`, `user_intervention_rate`

### A3: Prompt Signals → `a03_prompt_signals.json`
Per-session: `has_error_paste`, `has_log_paste`, `has_url`, `has_structured_data`, `has_code_paste`, `has_terminal_output`, `prompt_is_terse`, `first_prompt_char_len`

### A4: Session Outcomes → `a04_session_outcomes.json`
Per-session: `has_edits`, `edit_count`, `files_edited`, `has_git_commit`, `has_git_push`, `has_test_run`, `has_test_pass_after_fail`, `session_ended_with_user_msg`, `git_operations`

### A5: Undo/Revert → `a05_undo_revert.json`
Per-session: `user_requested_undo`, `undo_request_count`, `git_revert_in_session`, `ai_self_corrected`, `ai_self_correction_count`

### A6: AI Clarification → `a06_ai_clarification.json`
Per-session: `ai_asked_clarification`, `clarification_count`, `clarification_position`

### A7: Conversation Dynamics → `a07_conversation_dynamics.json`
Per-session: `total_turns`, `avg_ai_msgs_per_turn`, `deepest_turn`, `user_to_ai_char_ratio`, `session_duration_min`, `avg_gap_between_user_msgs_sec`, `longest_gap_sec`

### A8: Session Linkage → `a08_session_linkage.json`
Per-session: `is_continuation`, `compaction_count`, `time_gap_to_prev_session_min`, `is_sequential`, `has_parallel_sessions`, `parallel_same_repo`

### A9: Tool Profile → `a09_tool_profile.json`
Per-session: `explore_count`, `edit_count`, `shell_count`, `plan_count`, `delegation_count`, `web_count`, `mcp_count`, `explore_edit_ratio`, `unique_tools_used`, `dominant_tool_category`

### A10: Error Detection → `a10_error_detection.json`
Per-session: `shell_error_count`, `consecutive_shell_errors`, `error_cascade_count`, `error_then_user_rescue`

### B1: Task Classification (LLM) → `b01_task_classification.json`
Per-session: `task_type`, `task_complexity`, `task_domain`. Uses OpenAI gpt-4o-mini. Caches by prompt hash in `.llm_cache/`.

### B3: Prompt Quality (LLM) → `b03_prompt_quality.json`
Only for sessions with interrupts or clarifications. Per-session: `prompt_quality`, `prompt_improvement_suggestion`.

### B4: Failure Root Cause (LLM) → `b04_failure_root_cause.json`
Only for sessions with error cascades or undos. Per-session: `failure_root_cause`.

### C1: Developer Profile → `c01_developer_profile.json`
Aggregated: `sessions_per_active_day`, `preferred_source`, `avg_prompts_per_session`, `interrupt_rate`, `undo_rate`, `edit_session_pct`, `commit_session_pct`, `avg_autonomy_ratio`, etc.

### C2: Team Trends → `c02_team_trends.json`
Weekly: `sessions_this_week`, `edit_sessions_pct_trend`, `interrupt_rate_trend`, `avg_session_cost_trend`, etc.

## Worktree Strategy

Create worktrees off `run1-14022026-analytics` for parallel development:

| Worktree branch | Scope |
|---|---|
| `analyzers/foundation` | `config.py`, `base.py`, `runner.py`, `helpers.py` |
| `analyzers/heuristic-a1-a5` | A1 through A5 (interruptions, autonomy, prompts, outcomes, undo) |
| `analyzers/heuristic-a6-a10` | A6 through A10 (clarification, dynamics, linkage, tools, errors) |
| `analyzers/llm-and-cross` | B1, B3, B4, C1, C2 (LLM-based + cross-session) |

Each worktree works independently. Merge back to `run1-14022026-analytics` when done.

## Implementation Order

1. **Foundation first** — `config.py`, `base.py`, `helpers.py`, `runner.py`
2. **A1-A5 in parallel with A6-A10** — heuristic analyzers, no LLM dependency
3. **B+C last** — depend on A-series outputs for filtering (e.g., B3 needs A1 interrupt data)

## Verification

1. Ensure local Docker DB is running: `docker compose -f analysis-14022026/docker-compose.yml up -d`
2. Run: `python -m analyzers.runner --email <test_email> --since 2025-01-01 --until 2025-02-14`
3. Check output JSONs are valid and non-empty
4. Spot-check A1 interrupt counts against existing research script output
5. Run LLM analyzers: `--include-llm` and verify cache works

## Key Files to Reuse

- `analysis-14022026/utils/connection.py` — DB connection pattern
- `analysis-14022026/utils/sessions.py` — `by_user()` with date range
- `analysis-14022026/utils/messages.py` — `by_sessions()`
- `analysis-14022026/utils/tools.py` — `calls_by_session()`, `results_by_session()`
- `analysis-14022026/utils/pricing.py` — **`calc_cost(model, input_tokens, output_tokens, cached_tokens)`** for all cost calculations. Uses LiteLLM pricing with provider-prefix resolution + fallbacks. Do NOT reimplement cost logic.
- `analysis-14022026/utils/tokens.py` — token usage queries per session/user
- `analysis-14022026/session_view_scripts/research/06_interrupt_output_taxonomy.py` — interrupt parsing logic
- `analysis-14022026/session_view_scripts/research/ideation.md` — feature definitions (source of truth)
