Claude Code Plugins

Community-maintained marketplace

Feedback

context-assembler

@mehdic/CDC
0
0

Assembles relevant context for agent spawns with prioritized ranking. Ranks packages by relevance, enforces token budgets with graduated zones, captures error patterns for learning, and supports configurable per-agent retrieval limits.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name context-assembler
description Assembles relevant context for agent spawns with prioritized ranking. Ranks packages by relevance, enforces token budgets with graduated zones, captures error patterns for learning, and supports configurable per-agent retrieval limits.
version 1.5.3
allowed-tools Bash, Read

Context-Assembler Skill

You are the context-assembler skill. When invoked, you assemble relevant context packages for agent spawns, prioritizing by relevance and respecting token budgets.

When to Invoke This Skill

Invoke this skill when:

  • Orchestrator prepares to spawn an agent and needs relevant context
  • Any agent mentions "assemble context", "get context packages", or "context-assembler"
  • Preparing developer/QA/tech lead spawns with session context
  • Need to check for relevant error patterns before agent spawn

Do NOT invoke when:

  • No active orchestration session exists
  • Manually reading specific files (use Read tool directly)
  • Working outside BAZINGA orchestration

Your Task

When invoked, execute these steps in order:

Step 1: Determine Context Parameters

Extract from the calling request or infer from conversation:

  • session_id: Current orchestration session (REQUIRED)
  • group_id: Task group being processed (OPTIONAL - use empty string "" if not provided)
  • agent_type: Target agent - developer/senior_software_engineer/qa_expert/tech_lead/investigator (REQUIRED)
  • model: Model being used - haiku/sonnet/opus or full model ID (OPTIONAL, for token budgeting)
  • current_tokens: Current token usage in conversation (OPTIONAL, for zone detection)
  • iteration: Current iteration number (optional, default 0)
  • include_reasoning: Whether to include prior agent reasoning for handoff (OPTIONAL)
    • DEFAULT BEHAVIOR: Automatically true when reasoning context is beneficial:
      • qa_expert, tech_lead: ALWAYS (handoff recipients)
      • senior_software_engineer: ALWAYS (escalation needs prior context)
      • investigator: ALWAYS (debugging needs full context)
      • developer: When iteration > 0 (retry needs prior reasoning; first attempt has none)
    • Explicitly set to false to disable reasoning for any agent
  • reasoning_level: Level of detail for reasoning retrieval (OPTIONAL)
    • minimal: 400 tokens - key decisions only
    • medium: 800 tokens - decisions + approach (DEFAULT)
    • full: 1200 tokens - complete reasoning chain

If session_id or agent_type are missing, check recent conversation context or ask the orchestrator.

Step 2: Load Configuration and Check FTS5

Step 2a: Load retrieval limit for this agent type:

# Extract retrieval limit for the specific agent type
AGENT_TYPE="developer"  # Replace with actual agent_type

# Pass AGENT_TYPE via command-line argument (not string interpolation)
LIMIT=$(cat bazinga/skills_config.json 2>/dev/null | python3 -c "
import sys, json
agent = sys.argv[1] if len(sys.argv) > 1 else 'developer'
defaults = {'developer': 3, 'senior_software_engineer': 5, 'qa_expert': 5, 'tech_lead': 5, 'investigator': 5}
try:
    c = json.load(sys.stdin).get('context_engineering', {})
    limits = c.get('retrieval_limits', {})
    print(limits.get(agent, defaults.get(agent, 3)))
except:
    print(defaults.get(agent, 3))
" "$AGENT_TYPE" 2>/dev/null || echo 3)
echo "Retrieval limit for $AGENT_TYPE: $LIMIT"

Default limits: developer=3, senior_software_engineer=5, qa_expert=5, tech_lead=5, investigator=5

Step 2b: FTS5 availability:

FTS5 is assumed unavailable (requires special SQLite build). Always use heuristic fallback in Step 3b for ranking.

# FTS5 disabled by default - use heuristic ranking
FTS5_AVAILABLE="false"
echo "FTS5_AVAILABLE=$FTS5_AVAILABLE (heuristic fallback enabled)"

Step 2c: Determine token zone and budget:

# Token estimation with tiktoken (with fallback to character estimation)
# Input: MODEL, CURRENT_TOKENS (from Step 1)
MODEL="sonnet"  # or "haiku", "opus", or full model ID
CURRENT_TOKENS=0  # Current usage if known, else 0

# IMPORTANT: Use eval to capture output as shell variables
eval "$(python3 -c "
import sys, json

try:
    import tiktoken
    HAS_TIKTOKEN = True
except ImportError:
    HAS_TIKTOKEN = False

# Model context limits (conservative estimates)
MODEL_LIMITS = {
    'haiku': 200000, 'claude-3-5-haiku': 200000,
    'sonnet': 200000, 'claude-sonnet-4-20250514': 200000, 'claude-3-5-sonnet': 200000,
    'opus': 200000, 'claude-opus-4-20250514': 200000
}

# Read safety margin from config (default 15%)
try:
    with open('bazinga/skills_config.json') as f:
        cfg = json.load(f).get('context_engineering', {})
        SAFETY_MARGIN = cfg.get('token_safety_margin', 0.15)
except:
    SAFETY_MARGIN = 0.15

model = sys.argv[1] if len(sys.argv) > 1 else 'sonnet'
current = int(sys.argv[2]) if len(sys.argv) > 2 else 0

# Normalize model name (longest key first to avoid partial matches)
model_key = model.lower()
for key in sorted(MODEL_LIMITS.keys(), key=len, reverse=True):
    if key in model_key:
        model_key = key
        break

limit = MODEL_LIMITS.get(model_key, 200000)
effective_limit = int(limit * (1 - SAFETY_MARGIN))

# Calculate REMAINING budget (not total)
remaining_budget = max(0, effective_limit - current)
usage_pct = (current / effective_limit * 100) if effective_limit > 0 else 0

# Determine zone
if usage_pct >= 95:
    zone = 'Emergency'
elif usage_pct >= 85:
    zone = 'Wrap-up'
elif usage_pct >= 75:
    zone = 'Conservative'
elif usage_pct >= 60:
    zone = 'Soft_Warning'  # Underscore for shell variable safety
else:
    zone = 'Normal'

# Token cap logic (T042 Part C):
# - If orchestrator passes current_tokens (even 0 for first spawn), trust zone detection
# - Only apply conservative cap if invoked outside orchestrator context (safety fallback)
# The orchestrator now tracks: estimated_token_usage = total_spawns * 15000
# First spawn: 0 tokens, zone=Normal, full budget available - this is correct behavior

# Output as shell variable assignments (will be eval'd)
print(f'ZONE={zone}')
print(f'USAGE_PCT={usage_pct:.1f}')
print(f'EFFECTIVE_LIMIT={effective_limit}')
print(f'REMAINING_BUDGET={remaining_budget}')
print(f'HAS_TIKTOKEN={HAS_TIKTOKEN}')
" "$MODEL" "$CURRENT_TOKENS")"

# Now $ZONE, $USAGE_PCT, $EFFECTIVE_LIMIT, $REMAINING_BUDGET, $HAS_TIKTOKEN are set
echo "Zone: $ZONE, Usage: $USAGE_PCT%, Remaining: $REMAINING_BUDGET tokens"

Token Zone Behaviors:

Zone Usage % Behavior
Normal 0-60% Full context with all packages
Soft Warning 60-75% Prefer summaries over full content
Conservative 75-85% Minimal context, critical packages only
Wrap-up 85-95% Essential info only, no new packages
Emergency 95%+ Return immediately, suggest checkpoint

Token Budget Allocation by Agent Type:

Agent Task Specialization Context Pkgs Errors
developer 50% 20% 20% 10%
senior_software_engineer 40% 20% 25% 15%
qa_expert 40% 15% 30% 15%
tech_lead 30% 15% 40% 15%
investigator 35% 15% 35% 15%

Note: SSE and Investigator handle escalations/complex debugging, so they need more context and error budget.

Step 3: Query Context Packages (Zone-Conditional)

CRITICAL: Execute query based on zone from Step 2c

The query behavior depends entirely on the zone. Use this conditional structure:

# Zone-conditional query execution
# Variables from previous steps: $ZONE, $SESSION_ID, $GROUP_ID, $AGENT_TYPE, $LIMIT, $REMAINING_BUDGET

# Initialize result variable
QUERY_RESULT=""

if [ "$ZONE" = "Emergency" ]; then
    # Emergency zone: Skip all queries, go directly to Step 5
    echo "ZONE=Emergency: Skipping context query, proceeding to emergency output"
    QUERY_RESULT='{"packages":[],"total_available":0,"zone_skip":true}'

elif [ "$ZONE" = "Wrap-up" ]; then
    # Wrap-up zone: Skip context packages, minimal output only
    echo "ZONE=Wrap-up: Skipping context packages"
    QUERY_RESULT='{"packages":[],"total_available":0,"zone_skip":true}'

elif [ "$ZONE" = "Conservative" ]; then
    # Conservative zone: Priority fallback with LIMIT items across buckets
    echo "ZONE=Conservative: Using priority fallback ladder via bazinga-db"

    # Use bazinga-db get-context-packages command for each priority level
    QUERY_RESULT=$(python3 -c "
import subprocess
import json
import sys
import time

session_id = sys.argv[1]
group_id = sys.argv[2]
limit = int(sys.argv[3])
agent_type = sys.argv[4] if len(sys.argv) > 4 else 'developer'

def db_cmd_with_retry(cmd_args, max_retries=3, backoff_ms=[100, 250, 500]):
    '''Execute bazinga-db command with retry on database busy.'''
    for attempt in range(max_retries + 1):
        result = subprocess.run(cmd_args, capture_output=True, text=True)
        if result.returncode == 0:
            try:
                return json.loads(result.stdout) if result.stdout.strip() else []
            except json.JSONDecodeError:
                # Surface error rather than silently returning empty
                sys.stderr.write(f'JSON decode error: {result.stdout[:100]}\\n')
                return []
        if 'database is locked' in result.stderr or 'SQLITE_BUSY' in result.stderr:
            if attempt < max_retries:
                time.sleep(backoff_ms[attempt] / 1000.0)
                continue
        # Surface command errors
        if result.stderr:
            sys.stderr.write(f'Command error: {result.stderr[:200]}\\n')
        return []
    return []

# Priority fallback: Use bazinga-db to fetch packages by priority
# The get-context-packages command handles priority ordering internally
collected = db_cmd_with_retry([
    'python3', '.claude/skills/bazinga-db/scripts/bazinga_db.py', '--quiet',
    'get-context-packages', session_id, group_id, agent_type, str(limit)
])

# Handle result format
if isinstance(collected, dict):
    packages = collected.get('packages', [])
    total_available = collected.get('total_available', len(packages))
elif isinstance(collected, list):
    packages = collected
    total_available = len(packages)
else:
    packages = []
    total_available = 0

print(json.dumps({'packages': packages, 'total_available': total_available}))
" "$SESSION_ID" "$GROUP_ID" "$LIMIT" "$AGENT_TYPE")

else
    # Normal or Soft_Warning zone: Standard query
    echo "ZONE=$ZONE: Standard query with LIMIT=$LIMIT"
    QUERY_RESULT=$(python3 -c "
import subprocess
import json
import sys
import time

session_id = sys.argv[1]
group_id = sys.argv[2]
agent_type = sys.argv[3]
limit = int(sys.argv[4])

def db_query_with_retry(cmd_args, max_retries=3, backoff_ms=[100, 250, 500]):
    for attempt in range(max_retries + 1):
        result = subprocess.run(cmd_args, capture_output=True, text=True)
        if result.returncode == 0:
            try:
                return json.loads(result.stdout) if result.stdout.strip() else []
            except json.JSONDecodeError:
                return []
        if 'SQLITE_BUSY' in result.stderr or 'database is locked' in result.stderr:
            if attempt < max_retries:
                time.sleep(backoff_ms[attempt] / 1000.0)
                continue
        return []
    return []

# Use bazinga-db get-context-packages (parameterized, safe)
result = db_query_with_retry([
    'python3', '.claude/skills/bazinga-db/scripts/bazinga_db.py', '--quiet',
    'get-context-packages', session_id, group_id, agent_type, str(limit)
])

# If result is dict with 'packages' key, use it; otherwise wrap
if isinstance(result, dict):
    print(json.dumps(result))
elif isinstance(result, list):
    print(json.dumps({'packages': result, 'total_available': len(result)}))
else:
    print(json.dumps({'packages': [], 'total_available': 0}))
" "$SESSION_ID" "$GROUP_ID" "$AGENT_TYPE" "$LIMIT")
fi

# Parse result for next steps (log count only - summaries may contain secrets before redaction)
echo "Query returned: $(echo "$QUERY_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'{len(d.get(\"packages\",[]))} packages, total_available={d.get(\"total_available\",0)}')" 2>/dev/null || echo 'parse error')"

If query fails or returns empty, proceed to Step 3b (Heuristic Fallback).

Step 3b: Heuristic Fallback (Query Failed or FTS5 Unavailable)

First, fetch raw context packages with consumer data:

# Fetch packages with LEFT JOIN to get consumer info for agent_relevance calculation
SESSION_ID="bazinga_20250212_143530"
GROUP_ID="group_a"  # or empty string for session-wide
AGENT_TYPE="developer"

# Note: SESSION_ID is system-generated (not user input), but use shell variables for clarity
python3 .claude/skills/bazinga-db/scripts/bazinga_db.py --quiet query \
  "SELECT cp.id, cp.file_path, cp.priority, cp.summary, cp.group_id, cp.created_at,
          GROUP_CONCAT(cs.agent_type) as consumers
   FROM context_packages cp
   LEFT JOIN consumption_scope cs ON cp.id = cs.package_id AND cs.session_id = cp.session_id
   WHERE cp.session_id = '$SESSION_ID'
   GROUP BY cp.id"

Then apply heuristic ranking:

Priority Weight
critical 4
high 3
medium 2
low 1

Scoring Formula:

score = (priority_weight * 4) + (same_group_boost * 2) + (agent_relevance * 1.5) + recency_factor

Where:
- same_group_boost = 1 if package.group_id == request.group_id, else 0
- agent_relevance = 1 if AGENT_TYPE appears in package.consumers (from JOIN), else 0
- recency_factor = 1 / (days_since_created + 1)

Sort packages by score DESC, then by created_at DESC (tie-breaker), take top N. Calculate: overflow_count = max(0, total_packages - limit)

Step 3c: Token Packing with Redaction

After Step 3 or 3b retrieves packages, apply redaction, truncation, and token packing in the correct order:

# Token packing with proper order: redact → truncate → estimate → pack
# Input: $QUERY_RESULT (JSON from Step 3), $ZONE, $AGENT_TYPE, $REMAINING_BUDGET

PACKED_RESULT=$(python3 -c "
import json
import sys
import re

# Inputs from command line
query_result = json.loads(sys.argv[1])
zone = sys.argv[2]
agent_type = sys.argv[3]
remaining_budget = int(sys.argv[4])

packages = query_result.get('packages', [])
total_available = query_result.get('total_available', len(packages))

# --- Redaction Patterns (apply FIRST) ---
REDACTION_PATTERNS = [
    (r'(?i)(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|bearer)[\"\\s:=]+[\"\\']?([a-zA-Z0-9_\\-]{20,})[\"\\']?', r'\\1=[REDACTED]'),
    (r'(?i)(aws[_-]?(access|secret)[_-]?key[_-]?id?)[\"\\s:=]+[\"\\']?([A-Z0-9]{16,})[\"\\']?', r'\\1=[REDACTED]'),
    (r'(?i)(password|passwd|secret|private[_-]?key)[\"\\s:=]+[\"\\']?([^\\s\"\\'\n]{8,})[\"\\']?', r'\\1=[REDACTED]'),
    (r'(?i)(mongodb|postgres|mysql|redis|amqp)://[^\\s]+@', r'\\1://[REDACTED]@'),
    (r'eyJ[a-zA-Z0-9_-]*\\.eyJ[a-zA-Z0-9_-]*\\.[a-zA-Z0-9_-]*', '[JWT_REDACTED]'),
]

def redact_text(text):
    for pattern, replacement in REDACTION_PATTERNS:
        text = re.sub(pattern, replacement, text)
    return text

# --- Truncation limits per zone ---
SUMMARY_LIMITS = {
    'Normal': 400,
    'Soft_Warning': 200,
    'Conservative': 100,
    'Wrap-up': 60,
    'Emergency': 0
}

def truncate_summary(summary, zone):
    max_len = SUMMARY_LIMITS.get(zone, 400)
    if len(summary) <= max_len:
        return summary
    truncated = summary[:max_len].rsplit(' ', 1)[0]
    return truncated + '...'

# --- Token estimation ---
def estimate_tokens(text):
    # ~4 chars per token (conservative fallback)
    return len(text) // 4 + 1

# --- Budget allocation ---
CONTEXT_PCT = {
    'developer': 0.20,
    'senior_software_engineer': 0.25,
    'qa_expert': 0.30,
    'tech_lead': 0.40,
    'investigator': 0.35
}

pct = CONTEXT_PCT.get(agent_type, 0.20)
context_budget = int(remaining_budget * pct)  # Use REMAINING, not total

# --- Process packages: redact → truncate → estimate → pack ---
packed = []
used_tokens = 0
package_ids = []

for pkg in packages:
    raw_summary = pkg.get('summary', '')

    # 1. REDACT first
    redacted_summary = redact_text(raw_summary)

    # 2. TRUNCATE second
    truncated_summary = truncate_summary(redacted_summary, zone)

    # 3. ESTIMATE tokens
    pkg_text = f\"**[{pkg.get('priority', 'medium').upper()}]** {pkg.get('file_path', '')}\\n> {truncated_summary}\"
    pkg_tokens = estimate_tokens(pkg_text)

    # 4. PACK if within budget
    if used_tokens + pkg_tokens > context_budget:
        break

    packed.append({
        'id': pkg.get('id'),
        'file_path': pkg.get('file_path'),
        'priority': pkg.get('priority'),
        'summary': truncated_summary,
        'est_tokens': pkg_tokens
    })
    package_ids.append(pkg.get('id'))
    used_tokens += pkg_tokens

print(json.dumps({
    'packages': packed,
    'total_available': total_available,
    'used_tokens': used_tokens,
    'budget': context_budget,
    'package_ids': package_ids
}))
" "$QUERY_RESULT" "$ZONE" "$AGENT_TYPE" "$REMAINING_BUDGET")

# Extract package IDs for Step 5b consumption tracking (cast to strings to avoid TypeError)
PACKAGE_IDS=($(echo "$PACKED_RESULT" | python3 -c "import sys,json; ids=json.load(sys.stdin).get('package_ids',[]); print(' '.join(str(x) for x in ids))"))

echo "Packed: $(echo "$PACKED_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'{len(d.get(\"packages\",[]))} pkgs, {d.get(\"used_tokens\",0)}/{d.get(\"budget\",0)} tokens')")"
echo "Package IDs to mark consumed: ${PACKAGE_IDS[*]}"

Key improvements:

  • Uses REMAINING_BUDGET (not total limit)
  • Applies redaction BEFORE truncation
  • Populates PACKAGE_IDS array for Step 5b
  • Includes investigator in budget allocation

Step 3.5: Prior Reasoning Retrieval (Automatic for Handoffs)

When to include:

  • AUTOMATIC for qa_expert and tech_lead (handoff recipients in workflow)
  • OPTIONAL for other agents (only if Include Reasoning: true is explicit)
  • Can be disabled for any agent with Include Reasoning: false

Purpose: Retrieve prior agents' reasoning to provide continuity during handoffs (Developer→QA→Tech Lead).

Reasoning Levels (Token Budgets):

Level Tokens Content Use Case
minimal 400 Key decisions only Quick handoff, simple tasks
medium 800 Decisions + approach (DEFAULT) Standard handoffs
full 1200 Complete reasoning chain Complex tasks, debugging

Priority Order: completion > decisions > understanding (most actionable first).

Variable Setup: Determine reasoning inclusion based on agent type, iteration, and explicit overrides:

# Step 3.5 Variable Setup
# Automatic reasoning when context is beneficial

AGENT_TYPE="developer"  # From Step 1
ITERATION="${ITERATION:-0}"  # From Step 1 (default 0)

# Smart default: Enable reasoning when it provides value
# - qa_expert, tech_lead: ALWAYS (handoff recipients)
# - senior_software_engineer, investigator: ALWAYS (escalation/debugging needs context)
# - developer: Only on retry (iteration > 0); first attempt has no prior reasoning

case "$AGENT_TYPE" in
    qa_expert|tech_lead|senior_software_engineer|investigator)
        INCLUDE_REASONING="true"   # Always include for these agents
        ;;
    developer)
        if [ "$ITERATION" -gt 0 ]; then
            INCLUDE_REASONING="true"   # Retry needs prior reasoning
        else
            INCLUDE_REASONING="false"  # First attempt has no prior context
        fi
        ;;
    *)
        INCLUDE_REASONING="false"  # Unknown agents default off
        ;;
esac

# Check for explicit override in request (parse from Step 1)
# "Include Reasoning: false" -> disable even for QA/TL/SSE
# "Include Reasoning: true" -> enable even for developer first attempt
# "Reasoning Level: full" -> set REASONING_LEVEL

REASONING_LEVEL="medium"  # Default level
# If request contains "Reasoning Level: minimal" -> REASONING_LEVEL="minimal"
# If request contains "Reasoning Level: full" -> REASONING_LEVEL="full"
# Prior reasoning retrieval with level-based token budgets
# Variables: $SESSION_ID, $GROUP_ID, $AGENT_TYPE, $ITERATION, $INCLUDE_REASONING, $REASONING_LEVEL

# FIX 1: Validate iteration is a valid number (default to 0 if invalid)
validate_iteration() {
    local val="$1"
    if [[ "$val" =~ ^[0-9]+$ ]]; then
        echo "$val"
    else
        echo "0"  # Default to 0 for invalid input
    fi
}

ITERATION=$(validate_iteration "${ITERATION:-0}")

# Apply smart defaults if not explicitly set
if [ -z "$INCLUDE_REASONING" ]; then
    case "$AGENT_TYPE" in
        qa_expert|tech_lead|senior_software_engineer|investigator)
            INCLUDE_REASONING="true"
            ;;
        developer)
            if [ "$ITERATION" -gt 0 ]; then
                INCLUDE_REASONING="true"
            else
                INCLUDE_REASONING="false"
            fi
            ;;
        *)
            INCLUDE_REASONING="false"
            ;;
    esac
fi

REASONING_LEVEL="${REASONING_LEVEL:-medium}"

if [ "$INCLUDE_REASONING" = "true" ]; then
    echo "Retrieving prior reasoning for handoff context (level: $REASONING_LEVEL, iteration: $ITERATION)..."

    REASONING_DIGEST=$(python3 -c "
import sys
import json
import subprocess

session_id = sys.argv[1]
group_id = sys.argv[2] if len(sys.argv) > 2 else ''
reasoning_level = sys.argv[3] if len(sys.argv) > 3 else 'medium'
target_agent = sys.argv[4] if len(sys.argv) > 4 else 'unknown'

# Token budget based on reasoning level
LEVEL_BUDGETS = {
    'minimal': 400,
    'medium': 800,
    'full': 1200
}
max_tokens = LEVEL_BUDGETS.get(reasoning_level, 800)

# FIX 2: Relevance filtering - define which agents' reasoning is relevant for each target
# Workflow: Developer -> QA -> Tech Lead
# Escalation: Developer -> SSE, Developer -> Investigator
RELEVANT_AGENTS = {
    'qa_expert': ['developer', 'senior_software_engineer'],  # QA needs dev reasoning
    'tech_lead': ['developer', 'senior_software_engineer', 'qa_expert'],  # TL needs dev + QA
    'senior_software_engineer': ['developer'],  # SSE needs failed dev reasoning
    'investigator': ['developer', 'senior_software_engineer', 'qa_expert'],  # Investigator needs all
    'developer': ['developer', 'qa_expert', 'tech_lead'],  # Dev retry needs own + feedback
}
relevant_agents = RELEVANT_AGENTS.get(target_agent, [])

# FIX 3: Pruning limits for long retry chains
MAX_ENTRIES_PER_AGENT = 2  # Max 2 most recent entries per agent type
MAX_TOTAL_ENTRIES = 5  # Max 5 entries total regardless of agents

# Query reasoning from database via bazinga-db
# Priority order: completion > decisions > understanding (most actionable first)
PRIORITY_PHASES = ['completion', 'decisions', 'understanding']

try:
    # Get all reasoning for this session/group
    cmd = ['python3', '.claude/skills/bazinga-db/scripts/bazinga_db.py', '--quiet', 'get-reasoning', session_id]
    if group_id:
        cmd.extend(['--group_id', group_id])

    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(json.dumps({'error': 'query_failed', 'entries': [], 'used_tokens': 0}))
        sys.exit(0)

    entries = json.loads(result.stdout) if result.stdout.strip() else []
except Exception as e:
    print(json.dumps({'error': str(e), 'entries': [], 'used_tokens': 0}))
    sys.exit(0)

if not entries:
    print(json.dumps({'entries': [], 'used_tokens': 0, 'total_available': 0}))
    sys.exit(0)

# FIX 2: Filter to relevant agents only
if relevant_agents:
    entries = [e for e in entries if e.get('agent_type') in relevant_agents]

# FIX 3: Prune to MAX_ENTRIES_PER_AGENT per agent (most recent first)
# Group by agent, sort by timestamp desc, take top N per agent
from collections import defaultdict
agent_entries = defaultdict(list)
for entry in entries:
    agent_entries[entry.get('agent_type', 'unknown')].append(entry)

pruned_entries = []
for agent, agent_list in agent_entries.items():
    # Sort by timestamp descending (most recent first)
    agent_list.sort(key=lambda e: e.get('timestamp', ''), reverse=True)
    # Take only MAX_ENTRIES_PER_AGENT
    pruned_entries.extend(agent_list[:MAX_ENTRIES_PER_AGENT])

entries = pruned_entries

# Sort by priority phase, then by timestamp (most recent first within each phase)
def phase_priority(entry):
    phase = entry.get('phase', 'understanding')
    try:
        return PRIORITY_PHASES.index(phase)
    except ValueError:
        return len(PRIORITY_PHASES)  # Unknown phases last

# Two-pass sort: first by timestamp DESC, then stable sort by phase priority ASC
# This gives us most recent entries first within each phase
entries.sort(key=lambda e: e.get('timestamp', ''), reverse=True)  # timestamp DESC
entries.sort(key=phase_priority)  # phase priority ASC (stable sort preserves timestamp order)

# FIX 3: Apply total entry limit
entries = entries[:MAX_TOTAL_ENTRIES]

# Token estimation (~4 chars per token)
def estimate_tokens(text):
    return len(text) // 4 + 1 if text else 0

# Pack entries within budget
packed = []
used_tokens = 0

for entry in entries:
    content = entry.get('content', '')
    # Format: [agent] phase: content
    formatted = f\"[{entry.get('agent_type', 'unknown')}] {entry.get('phase', 'unknown')}: {content[:300]}\"
    entry_tokens = estimate_tokens(formatted)

    if used_tokens + entry_tokens > max_tokens:
        break

    packed.append({
        'agent_type': entry.get('agent_type'),
        'phase': entry.get('phase'),
        'content': content[:300] if len(content) > 300 else content,
        'confidence': entry.get('confidence_level'),
        'est_tokens': entry_tokens
    })
    used_tokens += entry_tokens

print(json.dumps({
    'entries': packed,
    'used_tokens': used_tokens,
    'budget': max_tokens,
    'level': reasoning_level,
    'total_available': len(entries),
    'relevant_agents': relevant_agents,
    'pruning': {'max_per_agent': MAX_ENTRIES_PER_AGENT, 'max_total': MAX_TOTAL_ENTRIES}
}))
" "$SESSION_ID" "$GROUP_ID" "$REASONING_LEVEL" "$AGENT_TYPE")

    echo "Reasoning digest: $(echo "$REASONING_DIGEST" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'{len(d.get(\"entries\",[]))} entries, {d.get(\"used_tokens\",0)}/{d.get(\"budget\",800)} tokens (level: {d.get(\"level\", \"medium\")})')" 2>/dev/null || echo 'parse error')"
else
    REASONING_DIGEST='{"entries":[],"used_tokens":0,"level":"none"}'
    echo "Skipping reasoning retrieval (include_reasoning=false for $AGENT_TYPE)"
fi

Output Format for Step 5:

If reasoning entries are found, include in output:

### Prior Agent Reasoning ({count} entries)

**[developer] completion:** Successfully implemented authentication using JWT...
**[qa_expert] decisions:** Chose to focus on edge cases for token expiration...

Only include if $INCLUDE_REASONING = true AND entries exist.

Step 4: Query Error Patterns (Optional)

If the agent previously failed or error patterns might be relevant:

Step 4a: Get project_id from session:

SESSION_ID="bazinga_20250212_143530"

# Retrieve project_id (defaults to 'default' if not set)
PROJECT_ID=$(python3 .claude/skills/bazinga-db/scripts/bazinga_db.py --quiet query \
  "SELECT COALESCE(json_extract(metadata, '\$.project_id'), 'default') as pid FROM sessions WHERE session_id = '$SESSION_ID'" \
  2>/dev/null | python3 -c "import sys,json; r=json.load(sys.stdin); print(r[0]['pid'] if r else 'default')" 2>/dev/null || echo "default")

Step 4b: Query matching error patterns:

# Filter by project_id and optionally session_id for more specific matches
python3 .claude/skills/bazinga-db/scripts/bazinga_db.py --quiet query \
  "SELECT signature_json, solution, confidence, occurrences FROM error_patterns WHERE project_id = '$PROJECT_ID' AND confidence > 0.7 ORDER BY confidence DESC, occurrences DESC LIMIT 3"

Only include patterns with confidence > 0.7 in the output.

Step 5: Format Output

Compute display values:

  • count = number of packages returned (up to limit)
  • available = total_available from Step 3 response (or total from Step 3b query)
  • overflow_count = max(0, available - count)
  • zone = current token zone from Step 2c
  • usage_pct = token usage percentage from Step 2c

Micro-Summary Truncation:

Apply zone-specific summary length limits for actual degradation:

Zone Max Summary Chars Rationale
Normal 400 Full detail
Soft Warning 200 Reduced detail
Conservative 100 Key points only
Wrap-up 60 Minimal hints
def truncate_summary(summary: str, zone: str) -> str:
    """Truncate summary based on zone-specific limits."""
    limits = {
        'Normal': 400,
        'Soft_Warning': 200,  # Underscore to match $ZONE variable
        'Conservative': 100,
        'Wrap-up': 60,
        'Emergency': 0  # No summaries in emergency
    }
    max_len = limits.get(zone, 400)
    if len(summary) <= max_len:
        return summary
    # Truncate at word boundary with ellipsis
    truncated = summary[:max_len].rsplit(' ', 1)[0]
    return truncated + '...'

Apply truncate_summary() to each package summary before rendering output.

Summary Redaction (Security):

Apply the same redaction patterns used for error_patterns to summaries before output:

import re

# Redaction patterns for secrets (same as error_patterns redaction)
REDACTION_PATTERNS = [
    # API keys and tokens
    (r'(?i)(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|bearer)["\s:=]+["\']?([a-zA-Z0-9_\-]{20,})["\']?', r'\1=[REDACTED]'),
    # AWS credentials
    (r'(?i)(aws[_-]?(access|secret)[_-]?key[_-]?id?)["\s:=]+["\']?([A-Z0-9]{16,})["\']?', r'\1=[REDACTED]'),
    # Passwords and secrets
    (r'(?i)(password|passwd|secret|private[_-]?key)["\s:=]+["\']?([^\s"\']{8,})["\']?', r'\1=[REDACTED]'),
    # Connection strings
    (r'(?i)(mongodb|postgres|mysql|redis|amqp)://[^\s]+@', r'\1://[REDACTED]@'),
    # JWT tokens
    (r'eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*', '[JWT_REDACTED]'),
]

def redact_summary(summary: str) -> str:
    """Redact potential secrets from summary."""
    redacted = summary
    for pattern, replacement in REDACTION_PATTERNS:
        redacted = re.sub(pattern, replacement, redacted)

    # Entropy-based detection for high-entropy strings (potential secrets)
    def has_high_entropy(s):
        if len(s) < 20:
            return False
        char_set = set(s)
        # High entropy = many unique chars relative to length
        return len(char_set) / len(s) > 0.6 and any(c.isdigit() for c in s) and any(c.isupper() for c in s)

    # Find and redact high-entropy strings
    words = redacted.split()
    for i, word in enumerate(words):
        if has_high_entropy(word):
            words[i] = '[REDACTED]'
    return ' '.join(words)

Apply redact_summary() before truncate_summary() in the processing pipeline.

Zone-Specific Output:

Emergency Zone (95%+):

## Context for {agent_type}

🚨 **Token budget: Emergency ({usage_pct}%) - Checkpoint recommended**

Context assembly skipped due to token budget constraints.
Suggest: Complete current operation and start new session.

Wrap-up Zone (85-95%):

## Context for {agent_type}

🔶 **Token budget: Wrap-up ({usage_pct}%) - Completing current operation**

### Essential Info Only

Minimal context mode active. Focus on completing current task.

Conservative Zone (75-85%):

## Context for {agent_type}

🔶 **Token budget: Conservative ({usage_pct}%)**

### Priority Packages ({count}/{available}) - {priority_used} level

**[{PRIORITY}]** {file_path}
> {summary}

Note: priority_used comes from the fallback ladder response (critical/high/medium).

Soft Warning Zone (60-75%):

## Context for {agent_type}

🔶 **Token budget: Soft Warning ({usage_pct}%) - Reduced summaries (200 char)**

### Relevant Packages ({count}/{available})

**[{PRIORITY}]** {file_path}
> {summary}  ← Truncated to 200 chars

Normal Zone (0-60%):

## Context for {agent_type}

### Relevant Packages ({count}/{available})

**[{PRIORITY}]** {file_path}
> {summary}

**[{PRIORITY}]** {file_path}
> {summary}

### Prior Agent Reasoning ({reasoning_count} entries)
<!-- Only include if include_reasoning=true AND entries exist -->

**[developer] completion:** Successfully implemented the core logic with edge case handling...
**[qa_expert] decisions:** Focused test coverage on authentication flow boundaries...

### Error Patterns ({pattern_count} matches)

⚠️ **Known Issue**: "{error_signature}"
> **Solution**: {solution}
> **Confidence**: {confidence} (seen {occurrences} times)

📦 +{overflow_count} more packages available (re-invoke with higher limit to expand)

Priority Indicators:

  • [CRITICAL] - Priority: critical
  • [HIGH] - Priority: high
  • [MEDIUM] - Priority: medium
  • [LOW] - Priority: low

Zone Indicators:

  • Normal zone: No indicator (full context)
  • Soft Warning/Conservative/Wrap-up: 🔶 (orange diamond)
  • Emergency: 🚨 (emergency symbol)

Only show overflow indicator if overflow_count > 0 AND zone is Normal or Soft Warning.

Step 5b: Mark Packages as Consumed (consumption_scope table)

IMPORTANT: Only run if zone is Normal or Soft_Warning (skip for Wrap-up/Emergency)

After formatting output, mark delivered packages as consumed in the consumption_scope table to prevent repeated delivery and enable iteration-aware tracking:

# Only mark consumption if packages were actually delivered
if { [ "$ZONE" = "Normal" ] || [ "$ZONE" = "Soft_Warning" ]; } && [ ${#PACKAGE_IDS[@]} -gt 0 ]; then
    # Mark consumed packages using bazinga-db mark-context-consumed command
    marked=0
    for pkg_id in "${PACKAGE_IDS[@]}"; do
        if python3 .claude/skills/bazinga-db/scripts/bazinga_db.py --quiet \
            mark-context-consumed "$pkg_id" "$AGENT_TYPE" "$ITERATION" 2>/dev/null; then
            marked=$((marked + 1))
        fi
    done
    echo "Marked $marked/${#PACKAGE_IDS[@]} packages as consumed via bazinga-db"
else
    echo "Skipping consumption tracking (zone=$ZONE or no packages)"
fi

Key features:

  • Uses bazinga-db mark-context-consumed command (proper skill invocation)
  • Handles retry logic internally within bazinga-db skill
  • Iteration-aware tracking per data-model.md
  • Skips in Wrap-up/Emergency zones (nothing delivered)

Step 6: Handle Edge Cases

Empty Packages: If no context packages are found (count=0, available=0):

## Context for {agent_type}

### Relevant Packages (0/0)

No context packages found for this session/group. The agent will proceed with task and specialization context only.

Graceful Degradation: If ANY step fails (database unavailable, query error, etc.):

  1. Log a warning (but do NOT block execution)
  2. Return minimal context:
## Context for {agent_type}

:warning: Context assembly encountered an error. Proceeding with minimal context.

**Fallback Mode**: Task and specialization context only. Context packages unavailable.
  1. CRITICAL: The orchestrator should NEVER block on context-assembler failure

Step 7: Strategy Extraction (Success Path)

When: Triggered after a task group completes successfully (Tech Lead APPROVED status).

Purpose: Extract and save successful approaches to the strategies table for future agent guidance.

Trigger Conditions

Strategy extraction should run when:

  • Tech Lead returns APPROVED status for a group
  • Developer completes without needing escalation
  • QA passes all tests on first attempt

Strategy Extraction Process

Note: Strategy extraction is triggered by the orchestrator (phase_simple.md, phase_parallel.md) after Tech Lead approval using the bazinga-db extract-strategies command:

bazinga-db, please extract strategies:

Session ID: {session_id}
Group ID: {group_id}
Project ID: {project_id}
Lang: {detected_lang}
Framework: {detected_framework}

Then invoke: Skill(command: "bazinga-db")

What the command does:

  1. Queries agent_reasoning table for completion/decisions/approach phases
  2. Maps phases to topics: completion→implementation, decisions→architecture, approach→methodology
  3. Generates deterministic strategy_id = {project_id}_{topic}_{content_hash}
  4. Upserts to strategies table (increments helpfulness if exists)
  5. Returns count of extracted strategies

Strategy Schema Reference

Field Type Description
strategy_id TEXT PK Unique identifier (project_topic_hash)
project_id TEXT Project this strategy applies to
topic TEXT Category: implementation, architecture, methodology
insight TEXT The actual insight/approach (max 500 chars)
helpfulness INT Usage counter, incremented on reuse
lang TEXT Language context (python, typescript, etc.)
framework TEXT Framework context (react, fastapi, etc.)
last_seen TEXT Last time strategy was applied
created_at TEXT When strategy was first captured

Strategy Retrieval for Context

When assembling context, strategies can be queried for relevant hints:

SELECT topic, insight FROM strategies
WHERE project_id = ?
  AND (lang IS NULL OR lang = ?)
  AND (framework IS NULL OR framework = ?)
ORDER BY helpfulness DESC, last_seen DESC
LIMIT 3

Configuration Reference

From bazinga/skills_config.json:

{
  "context_engineering": {
    "enable_context_assembler": true,
    "enable_fts5": false,
    "retrieval_limits": {
      "developer": 3,
      "senior_software_engineer": 5,
      "qa_expert": 5,
      "tech_lead": 5,
      "investigator": 5
    },
    "redaction_mode": "pattern_only",
    "token_safety_margin": 0.15
  }
}
Setting Default Description
enable_context_assembler true Enable/disable the skill
enable_fts5 false Use FTS5 for relevance (requires SQLite FTS5)
retrieval_limits.* 3 Max packages per agent type
redaction_mode pattern_only Secret redaction mode
token_safety_margin 0.15 Safety margin for token budgets

Example Invocations

Example 1: Developer Context Assembly

Request:

Assemble context for developer spawn:
- Session: bazinga_20250212_143530
- Group: group_a
- Agent: developer

Output:

## Context for developer

### Relevant Packages (3/7)

**[HIGH]** research/auth-patterns.md
> JWT authentication patterns for React Native apps

**[MEDIUM]** research/api-design.md
> REST API design guidelines for mobile clients

**[MEDIUM]** findings/codebase-analysis.md
> Existing authentication code in src/auth/

### Error Patterns (1 match)

:warning: **Known Issue**: "Cannot find module '@/utils'"
> **Solution**: Check tsconfig.json paths configuration - ensure baseUrl is set correctly
> **Confidence**: 0.8 (seen 3 times)

:package: +4 more packages available (re-invoke with higher limit to expand)

Example 2: Session-Wide Context (No Group)

Request:

Assemble context for tech_lead spawn:
- Session: bazinga_20250212_143530
- Group: (none - session-wide)
- Agent: tech_lead

Commands used:

python3 .claude/skills/bazinga-db/scripts/bazinga_db.py --quiet get-context-packages \
  "bazinga_20250212_143530" "" "tech_lead" 5

Example 3: Empty Context

Output:

## Context for qa_expert

### Relevant Packages (0/0)

No context packages found for this session/group. The agent will proceed with task and specialization context only.

Example 4: Error/Fallback

Output (if database unavailable):

## Context for tech_lead

:warning: Context assembly encountered an error. Proceeding with minimal context.

**Fallback Mode**: Task and specialization context only. Context packages unavailable.

Security Notes

Parameter Handling:

  • Always assign user-provided values to shell variables first
  • Use quoted variable expansion ("$VAR") in commands
  • The bazinga-db CLI uses positional arguments (safer than string interpolation)
  • Avoid constructing SQL strings with raw user input

Example of safe vs unsafe:

# SAFE: Use shell variables with quotes
SESSION_ID="user_provided_session"
python3 ... --quiet get-context-packages "$SESSION_ID" "$GROUP_ID" "$AGENT_TYPE" "$LIMIT"

# UNSAFE: Direct string interpolation (avoid this)
python3 ... --quiet query "SELECT * FROM t WHERE id = 'user_input'"

Integration with Orchestrator

The orchestrator invokes this skill before spawning agents:

# 1. Invoke context-assembler
Skill(command: "context-assembler")

# 2. Capture output and include in agent prompt
Task(
    prompt=f"""
    {context_assembler_output}

    ## Your Task
    {task_description}
    """,
    subagent_type="developer"
)

Database Tables Used

Table Purpose
context_packages Research files, findings, artifacts with priority/summary
consumption_scope Iteration-aware package consumption tracking (per data-model.md)
error_patterns Captured error signatures with solutions
strategies Successful approaches extracted from completed tasks (Step 7)
agent_reasoning Agent reasoning phases used for strategy extraction
sessions Session metadata including project_id

Note: The consumption_scope table has columns: scope_id, session_id, group_id, agent_type, iteration, package_id, consumed_at. Step 5b uses this for tracking delivery per session/group/agent/iteration to enable fresh context on retries.

Note: The strategies table is populated by Step 7 when tasks complete successfully. Strategies are queried during context assembly to provide insights from past successful implementations.


Performance (SC-005)

Target: Context assembly must complete in <500ms.

Estimated Performance:

Step Operation Time
Parse input Step 1 <5ms
Token zone detection Step 2 <5ms
Query packages Step 3 (indexed) 30-50ms
Token packing Step 3c 20-50ms
Query error patterns Step 4 (indexed) 30-50ms
Format output Step 5 <10ms
Mark consumption Step 5b 20-50ms
Total ~100-200ms

Performance Prerequisites:

  • SQLite WAL mode enabled (concurrent reads)
  • Indexes created on context_packages, error_patterns, consumption_scope
  • Retry backoff (100ms, 200ms, 400ms) adds max 700ms only if database locked

If performance degrades:

  1. Check PRAGMA journal_mode returns wal
  2. Verify indexes exist: SELECT name FROM sqlite_master WHERE type='index'
  3. Check for lock contention in parallel agent spawns

References

See references/usage.md for detailed usage documentation and integration examples.