Skip to content

chrisipanaque/opencode-builder-profile

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

opencode-builder-profile

Generate LLM-powered engineering profiles from OpenCode session data.
Built for talent discovery, hiring evaluation, and professional portfolio generation.

Quick Start · How It Works · Extending · Output Format · CLI Reference

Python 3.9+ License: MIT PRs Welcome


Overview

opencode-builder-profile reads an OpenCode SQLite database and produces a structured JSON engineering profile. The pipeline has three stages: extract compacted metrics from the session DB, dispatch per-dimension LLM calls via GitHub Models, and assemble the results into a validated profile document.

Each profile dimension — coding velocity, technical breadth, system design thinking, tool mastery, collaboration signals, growth trajectory — receives its own focused LLM call with only the relevant extracted data. This prevents context bleed and keeps each assessment sharp.

The final output is a JSON document consumable by frontend applications, portfolio builders, or directly by hiring teams.


Quick Start

git clone https://github.com/YOUR_USER/opencode-builder-profile.git
cd opencode-builder-profile
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# Configure your GitHub token for LLM access
cp .env.example .env
# Edit .env: set GITHUB_TOKEN=ghp_your_token_here

# Dry run: verify DB access (no LLM calls)
python opencode_builder_profile/ --dry-run

# Run a single extractor to inspect its compacted data
python opencode_builder_profile/ --extractor session_metrics --dry-run
python opencode_builder_profile/ --extractor tool_usage --dry-run

# Full profile generation (all extractors, all LLM calls)
python opencode_builder_profile/

The --dry-run flag extracts and saves compacted data to outputs/sections/compacted_*.json but skips all LLM calls — no GITHUB_TOKEN required. Use --extractor <name> to target a single dimension during development or debugging.


How It Works

Pipeline Architecture

                         ┌───────────────────┐
                         │   opencode.db     │
                         │  (SQLite via WAL) │
                         └────────┬──────────┘
                                  │
                    ┌─────────────▼─────────────┐
                    │   db/{sessions,messages,  │
                    │    todos,projects}/       │
                    │   raw SQL queries         │
                    └─────────────┬─────────────┘
                                  │
           ┌──────────────────────┼──────────────────────┐
           │                      │                      │
           ▼                      ▼                      ▼
  ┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
  │ session_metrics │   │  code_changes   │   │  tool_usage     │
  │ extractor       │   │  extractor      │   │  extractor      │
  └────────┬────────┘   └────────┬────────┘   └────────┬────────┘
           │                     │                      │
           ▼                     ▼                      ▼
  ┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
  │ compacted JSON  │   │ compacted JSON  │   │ compacted JSON  │
  │ (data, no LLM)  │   │ (data, no LLM)  │   │ (data, no LLM)  │
  └────────┬────────┘   └────────┬────────┘   └────────┬────────┘
           │                     │                      │
           ▼                     ▼                      ▼
  ┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
  │ LLM prompt v1   │   │ LLM prompt v2   │   │ LLM prompt v3   │
  │ "Assess coding  │   │ "Assess tech    │   │ "Assess tool    │
  │  velocity..."   │   │  breadth..."    │   │  mastery..."    │
  └────────┬────────┘   └────────┬────────┘   └────────┬────────┘
           │                     │                      │
           ▼                     ▼                      ▼
  ┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
  │ section JSON    │   │ section JSON    │   │ section JSON    │
  └────────┬────────┘   └────────┬────────┘   └────────┬────────┘
           │                     │                      │
           └─────────────────────┼──────────────────────┘
                                 │
                                 ▼
                    ┌──────────────────────────┐
                    │      assemble.py         │
                    │  merge + Pydantic val.   │
                    └───────────┬──────────────┘
                                │
                                ▼
                    ┌──────────────────────────┐
                    │  builder_profile.json    │
                    │  (validated, final)      │
                    └──────────────────────────┘

Data Flow Detail

Each extractor is an independent module at opencode_builder_profile/extractors/<name>.py. Every extractor exports two symbols:

  • NAME: str — maps to the matching prompt file at profile/prompts/<NAME>.txt
  • extract(conn: sqlite3.Connection) -> dict — runs focused queries against the DB, returns a compact summary

The CLI iterates EXTRACTORS, calls each extractor, saves the compacted data, then feeds it to the corresponding LLM prompt. The LLM response is parsed as JSON and saved as a section file. After all sections complete, a final narrative call synthesizes everything into an executive summary.

Example: Raw Data → LLM Call

Compacted data produced by session_metrics extractor for a real session history:

{
  "total_sessions": 95,
  "sessions_per_week": 18.2,
  "avg_duration_min": 884.1,
  "median_duration_min": 8.1,
  "peak_hour_utc": 15,
  "agent_distribution": [
    {"agent": "explore", "count": 40},
    {"agent": "build", "count": 34},
    {"agent": "plan", "count": 18},
    {"agent": "general", "count": 3}
  ],
  "aggregate_tokens": {
    "input": 30799090,
    "output": 2813990,
    "reasoning": 2293044
  },
  "total_cost": 2.78,
  "duration_list": [273.8, 0.8, 1.9, 11.0, 36.7, ...]
}

LLM prompt (profile/prompts/session_metrics.txt):

You are a talent assessment analyst evaluating an AI-assisted software
engineer's coding velocity and work patterns.

Based on the extracted session metrics data below, produce a JSON
assessment of this engineer's coding velocity and work patterns.

Output ONLY valid JSON with this exact structure:
{
  "sessions_per_week": <float>,
  "avg_session_duration_min": <float>,
  "consistency_score": <float between 0 and 1>,
  "assessment": "...",
  "strengths": [...],
  "signals": [...]
}

Be honest and critical. Low session counts or irregular patterns should
be reflected in low consistency scores.

LLM response saved as outputs/sections/session_metrics.json:

{
  "sessions_per_week": 18.2,
  "avg_session_duration_min": 14.7,
  "consistency_score": 0.72,
  "assessment": "High session volume suggests consistent daily engagement with the tool. The wide gap between mean and median indicates a mix of quick tasks and deep-dive sessions, typical of an engineer who handles both exploratory research and sustained implementation work.",
  "strengths": ["Consistent daily engagement across 5-week span", "High session volume (18+/week)"],
  "signals": ["Peak activity at 15:00 UTC suggests afternoon depth work", "Mix of sub-minute and multi-hour sessions indicates varied task complexity"]
}

Profile Dimensions

Dimension Extractor Data Sources LLM Evaluates
Coding Velocity session_metrics Session count, durations, hourly/weekly distribution, token volumes Work pace consistency, peak productivity windows, engagement patterns
Technical Breadth code_changes File diffs, language distribution, additions/deletions, most-changed files Language versatility, codebase exploration patterns, depth vs. breadth
System Design Thinking agent_workflow Agent switching sequence, sub-agent depth, agent distribution Problem decomposition strategy, delegation patterns, architectural reasoning
Tool Mastery tool_usage Tool call counts, read/edit/bash ratios, unique tools, sample commands Workflow efficiency, tool diversity, exploration vs. execution balance
Collaboration conversation Prompt lengths, reasoning parts, user/assistant message counts, sample prompts Communication clarity, reasoning engagement, specification quality
Growth Trajectory project_diversity Distinct projects, session distribution, project names, directories Learning velocity, complexity progression, skill breadth
Task Execution task_completion Todo completion rate, priority distribution, sample tasks Planning discipline, follow-through, priority management
Narrative Summary synthesis All section outputs above Executive synthesis, hiring recommendation, risk factors

Output

Directory Structure

outputs/
├── sections/
│   ├── compacted_session_metrics.json     # Raw extractor output: durations, counts, distributions
│   ├── session_metrics.json               # LLM assessment: velocity scores, consistency, signals
│   ├── compacted_code_changes.json        # Raw extractor output: files, languages, diffs
│   ├── code_changes.json                  # LLM assessment: breadth score, depth signals
│   ├── compacted_tool_usage.json          # Raw extractor output: tool calls, ratios
│   ├── tool_usage.json                    # LLM assessment: efficiency, workflow patterns
│   ├── compacted_agent_workflow.json      # Raw extractor output: switches, sub-agents
│   ├── agent_workflow.json                # LLM assessment: design thinking, delegation
│   ├── compacted_conversation.json        # Raw extractor output: prompts, reasoning
│   ├── conversation.json                  # LLM assessment: clarity, engagement
│   ├── compacted_project_diversity.json   # Raw extractor output: projects, directories
│   ├── project_diversity.json             # LLM assessment: growth trajectory
│   ├── compacted_task_completion.json     # Raw extractor output: todos, completion
│   ├── task_completion.json               # LLM assessment: discipline, planning
│   └── narrative_summary.json             # LLM synthesis: executive summary
└── builder_profile.json                   # Merged, Pydantic-validated final profile

Final Profile Schema (builder_profile.json)

{
  "meta": {
    "generated_at": "2026-07-01T20:13:45+00:00",
    "total_sessions": 95,
    "total_projects": 20,
    "sections_generated": [
      "session_metrics", "code_changes", "tool_usage",
      "agent_workflow", "conversation", "project_diversity",
      "task_completion", "narrative_summary"
    ]
  },
  "coding_velocity": {
    "raw": {
      "sessions_per_week": 18.2,
      "consistency_score": 0.72,
      "assessment": "...",
      "strengths": [...],
      "signals": [...]
    }
  },
  "technical_breadth": { "raw": { ... } },
  "system_design_thinking": { "raw": { ... } },
  "tool_mastery": { "raw": { ... } },
  "collaboration": { "raw": { ... } },
  "growth": { "raw": { ... } },
  "narrative_summary": { "raw": { ... } }
}

Each dimension's raw field contains the exact JSON output from its LLM call. The schema is defined as a Pydantic BaseModel in opencode_builder_profile/profile/schema.py and validated before write.


Extending

Adding a New Profile Dimension

The system is designed so that adding a new dimension requires touching exactly four locations — three of which are single-line additions:

1. opencode_builder_profile/extractors/foo.py       # 25-50 lines
2. opencode_builder_profile/extractors/__init__.py   # +1 import
3. opencode_builder_profile/profile/prompts/foo.txt  # system prompt
4. opencode_builder_profile/profile/schema.py         # optional: add section enum entry

Step-by-step:

  1. Create the extractor at extractors/foo.py:
NAME = "error_recovery"

def extract(conn):
    """Query the DB for rejection/retry patterns."""
    rejected_count = conn.execute(
        "SELECT COUNT(*) FROM part WHERE json_extract(data, '$.type') = 'tool' AND json_extract(data, '$.state.status') = 'error'"
    ).fetchone()[0]
    return {
        "total_errors": rejected_count,
        "recovery_rate": 0.85,
    }
  1. Register it in extractors/__init__.py:
EXTRACTORS = [
    session_metrics,
    code_changes,
    tool_usage,
    agent_workflow,
    conversation,
    project_diversity,
    task_completion,
    error_recovery,        # ← new
]
  1. Write the prompt at profile/prompts/error_recovery.txt:
You are a talent assessment analyst evaluating an AI-assisted software
engineer's error recovery and debugging patterns.

Based on the extracted error data below, assess their resilience,
debugging methodology, and ability to recover from failures.

Output ONLY valid JSON with this exact structure:
{
  "error_frequency": <string>,
  "recovery_efficiency": <float between 0 and 1>,
  "debugging_approach": "...",
  "assessment": "...",
  "strengths": [...],
  "development_areas": [...]
}

That's it. No config files, no CLI changes, no wiring beyond the registry import. The CLI auto-discovers EXTRACTORS and maps each extractor's NAME to profile/prompts/{NAME}.txt.

Removing a Dimension

Delete the extractor file and remove its import from extractors/__init__.py. The CLI will skip it cleanly.


Configuration

Environment Variables (.env)

Variable Required Default Description
GITHUB_TOKEN Yes GitHub Personal Access Token for GitHub Models API. Create at github.com/settings/tokens — no special scopes needed.
GITHUB_MODEL No gpt-4o-mini Model name for all LLM calls. Accepts any model available through the GitHub Models endpoint.
OPENCODE_DB_PATH No ~/.local/share/opencode/opencode.db Override path to opencode.db. Useful for non-standard installs or inspecting a different user's database.
OUTPUT_DIR No ./outputs Directory for all output files (sections/ and builder_profile.json).

CLI Arguments

Flag Override
--db PATH OPENCODE_DB_PATH
--model NAME GITHUB_MODEL
--output DIR OUTPUT_DIR
--dry-run Skip all LLM calls — extract and save compacted data only

Database Auto-Detection

The tool searches for opencode.db in the default XDG data directory (~/.local/share/opencode/). If your OpenCode installation uses a custom path (visible in OpenCode's settings or via echo $OPENCODE_DB), pass --db or set OPENCODE_DB_PATH.


CLI Reference

python opencode_builder_profile/ [OPTIONS]

Options:
  --dry-run         Extract data and save compacted metrics only.
                    No LLM calls. No GITHUB_TOKEN required.
  --extractor NAME  Run a single extractor by name (e.g. session_metrics).
                    Combine with --dry-run to inspect raw data without an LLM call.
  --db PATH         Path to opencode.db (overrides env var).
  --model NAME      GitHub Models model (overrides env var).
                    Default: gpt-4o-mini
  --output DIR      Output directory (overrides env var).
                    Default: ./outputs

Examples:
  # Verify your DB is accessible
  python opencode_builder_profile/ --dry-run

  # Inspect compacted data for one dimension
  python opencode_builder_profile/ --extractor session_metrics --dry-run

  # Use a specific DB and model
  python opencode_builder_profile/ --db /custom/path/opencode.db --model gpt-4o

  # Output to a specific directory
  python opencode_builder_profile/ --output ./my-builder-profile

Project Structure

opencode-builder-profile/
├── pyproject.toml                                    # Project metadata
├── requirements.txt                                  # Runtime dependencies
├── .env.example                                      # Configuration template
├── opencode_builder_profile/
│   ├── __init__.py
│   ├── __main__.py                                   # Entry point
│   ├── cli.py                                        # CLI orchestration
│   ├── config.py                                     # Env loading, DB detection
│   ├── utils.py                                      # Shared helpers
│   ├── db/
│   │   ├── connection.py                             # SQLite connection (WAL, Row factory)
│   │   ├── sessions.py                               # Session queries: aggregates, distributions
│   │   ├── messages.py                               # Message/part queries: texts, tool calls, diffs
│   │   ├── todos.py                                  # Todo queries: completion rates, priorities
│   │   └── projects.py                               # Project queries: counts, worktrees
│   ├── extractors/
│   │   ├── __init__.py                               # EXTRACTORS registry — add new extractors here
│   │   ├── session_metrics.py                        # Duration, frequency, consistency
│   │   ├── code_changes.py                           # Languages, files, diff volumes
│   │   ├── tool_usage.py                             # Tool diversity, read/edit/bash ratios
│   │   ├── agent_workflow.py                         # Agent switching, sub-agent delegation
│   │   ├── conversation.py                           # Prompt/reasoning analysis
│   │   ├── project_diversity.py                      # Project breadth, tech stacks
│   │   └── task_completion.py                        # Todo completion, priority distribution
│   └── profile/
│       ├── client.py                                 # Requests-based GitHub Models API client
│       ├── prompts/                                  # One .txt prompt per extractor
│       │   ├── session_metrics.txt
│       │   ├── code_changes.txt
│       │   ├── tool_usage.txt
│       │   ├── agent_workflow.txt
│       │   ├── conversation.txt
│       │   ├── project_diversity.txt
│       │   └── task_completion.txt
│       ├── assemble.py                               # Prompt templates, section mapping
│       └── schema.py                                 # Pydantic models for final profile
└── outputs/                                          # Generated (gitignored)
    ├── sections/                                     # Per-dimension LLM responses
    └── builder_profile.json                          # Merged final profile

Technical Decisions

Why One LLM Call Per Dimension

A single large call with all data produces shallower analysis — the LLM's attention is diluted across irrelevant metrics. Per-dimension calls keep context windows focused, allow independent prompt engineering, and limit blast radius: if one LLM call fails, only that dimension is skipped.

Why Compact Before LLM

The raw DB output for 95 sessions includes millions of rows across message and part tables. Passing this raw data would exceed LLM context limits. Each extractor aggregates into a compacted summary (1-5 KB) before the LLM call. This also means the DB queries are the only expensive operation — LLM calls stay cheap and fast.

Why Pydantic for Output Only

The intermediate compacted dicts are unvalidated by design — extractors are free to shape their output however makes sense. Pydantic is reserved for the final builder_profile.json, which must conform to a stable schema for frontend consumption. The schema can be exported as JSON Schema via BuilderProfile.model_json_schema().

Why GitHub Models

GitHub Models provides free API access with any GitHub PAT (no billing setup required). The endpoint is OpenAI-compatible, so swapping to another provider (OpenAI, Anthropic, etc.) requires changing only profile/client.py.


Comparison to Alternative Approaches

Approach Data Resolution LLM Call Efficiency Extensibility Dependencies
Single monolithic prompt Medium — all data in one context Poor — large context, diluted attention Low — any change retrains all 1 call
Per-dimension (this project) High — each dimension gets full attention Good — small contexts, focused prompts High — add/remove dimensions independently N calls (N = dimensions)
No LLM, rule-based scoring Low — heuristic approximations N/A Medium — requires code for each new metric 0 calls

License

MIT

About

Uses your OpenCode SQLite database and LLMs to create your own builder profile based on coding velocity, system design thinking, tool mastery, and growth trajectory for your own engineering evaluation.

Topics

Resources

Stars

16 stars

Watchers

0 watching

Forks

Contributors

Languages