
Self Improving Agent
- 210 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Design agents that learn from failures, update prompts or tools, and refine behavior over sessions without manual rewrites after each mistake.
About
Guides construction of self-improving Claude agents that capture errors, distill lessons, and update strategies over time—ideal for long-running assistants that must adapt without constant human prompt surgery.
- Adds reflection and feedback loops to agent runs
- Persists lessons learned across sessions
- Tunes prompts and tool selection from outcomes
- Reduces repeated human correction cycles
- Supports eval-driven iteration on agent behavior
Self Improving Agent by the numbers
- 210 all-time installs (skills.sh)
- Ranked #2,816 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill self-improving-agentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 210 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Design agents that learn from failures, update prompts or tools, and refine behavior over sessions without manual rewrites after each mistake.
Files
Self-Improving Agent - Autonomous Learning Patterns
Architectural patterns for AI agents that get better with use. Most agents are stateless -- they repeat mistakes because they cannot learn from their own execution. This skill closes that gap with patterns for feedback capture, memory curation, skill extraction, and regression detection. Key insight: auto-memory captures everything, but curation turns noise into knowledge.
Core Capabilities
- Memory curation — a layered memory stack (CLAUDE.md → MEMORY.md → session), review protocol, and promotion criteria for graduating learnings into enforced rules.
- Feedback loops — outcome classification, signal extraction, and a capture template that turn every task result into a structured learning.
- Regression detection — metrics, thresholds, and a response protocol that flags performance degradation within a few sessions.
- Skill extraction — criteria and a 4-step process to graduate proven patterns into standalone skill packages.
- Meta-learning — adaptive capture strategy and anti-pattern detection so the agent learns what is worth learning.
- Continuous calibration — confidence scoring and belief revision for resolving contradictions across learned knowledge.
When to Use
- Building agents intended to improve over time rather than stay stateless.
- Managing auto-memory (MEMORY.md) and deciding what to keep, promote, or retire.
- Designing self-correcting feedback loops and regression alarms for agent behavior.
- Graduating recurring solutions into reusable skill packages.
Sub-Skills
Compound sub-skill architecture — each file in skills/ handles one step of the improvement loop:
| Sub-Skill | File | Purpose |
|---|---|---|
| Remember | skills/remember.md | Capture errors and learnings from current session |
| Extract | skills/extract.md | Extract reusable patterns from completed work |
| Promote | skills/promote.md | Graduate proven patterns to permanent rules |
| Review | skills/review.md | Audit memory health, prune stale entries |
| Status | skills/status.md | Dashboard showing memory state and learning progress |
Flow: Remember → Extract → Promote → Review, with Status providing visibility back into the cycle.
Tools
| Tool | Purpose | Command |
|---|---|---|
pattern_extractor.py | Extract reusable patterns from session logs | python scripts/pattern_extractor.py --input sessions.jsonl --min-occurrences 3 |
memory_health_checker.py | Audit memory for line counts, stale, and promotable entries | python scripts/memory_health_checker.py --memory ./MEMORY.md --rules ./.claude/rules/ |
rule_promoter.py | Validate and apply promotions from memory to rules | python scripts/rule_promoter.py --memory ./MEMORY.md --list-candidates |
feedback_analyzer.py | Analyze feedback logs for success rates and opportunities | python scripts/feedback_analyzer.py analyze |
regression_detector.py | Compare baseline vs current performance metrics | python scripts/regression_detector.py compare |
rule_manager.py | Manage a learned rules knowledge base with CRUD | python scripts/rule_manager.py list |
References
Load the reference that matches the task — keep this file lean and pull detail on demand:
- [references/memory-curation-guide.md](references/memory-curation-guide.md) — the memory stack, review protocol, promotion criteria/targets, the Weekly Memory Health Check workflow, and the continuous-calibration (confidence scoring + belief revision) machinery. Read when curating MEMORY.md or promoting learnings to rules.
- [references/feedback-loop-patterns.md](references/feedback-loop-patterns.md) — the core improvement-loop architecture and maturity levels, outcome classification + signal extraction, the capture template, regression metrics/response, the post-session and regression-investigation workflows, common pitfalls, troubleshooting, and the success-criteria bar. Read when designing feedback capture or diagnosing a regression.
- [references/meta-learning-architectures.md](references/meta-learning-architectures.md) — skill-extraction criteria and process, the adaptive capture strategy, and anti-pattern detection. Read when the agent should adapt its own learning strategy or extract a proven pattern into a skill.
- [references/self-improvement-methodology.md](references/self-improvement-methodology.md) — the five layers of agent learning, the confidence-scoring model, the promotion decision tree, the memory-curation checklist, anti-patterns, and the metrics/thresholds table. Read for the end-to-end methodology overview.
Scope & Limitations
This skill covers:
- Architectural patterns for building agents that learn from execution history and user feedback.
- Memory lifecycle management: capture, curation, promotion, and retirement of learned knowledge.
- Performance regression detection frameworks and response protocols for agent systems.
- Skill extraction methodology for graduating proven patterns into reusable, standalone packages.
This skill does NOT cover:
- Runtime agent orchestration or multi-agent coordination -- see
agent-workflow-designerandagent-protocol. - Prompt engineering, testing, or versioning of the prompts themselves -- see
prompt-engineer-toolkit. - Infrastructure-level observability (logging, tracing, alerting dashboards) -- see
observability-designer. - Initial agent architecture design, tool selection, or capability planning -- see
agent-designer.
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
| context-engine | Controls what the agent sees per session; this skill decides what is worth remembering long-term | Promoted rules and curated memory feed context retrieval; context relevance metrics flow back for regression tracking |
| agent-designer | Defines the agent's architecture and capabilities; this skill layers learning infrastructure on top | Architecture constraints inform possible feedback loops; extracted skills feed back as new capabilities |
| prompt-engineer-toolkit | Prompts degrade as codebases evolve; this skill detects prompt regression via outcome tracking | Performance metrics flag underperforming prompts; prompt updates feed back as CLAUDE.md rule changes |
| observability-designer | Provides system-level metrics; this skill provides agent-behavior-level metrics | System telemetry enriches regression diagnosis; agent metrics export to observability dashboards |
| tech-debt-tracker | Stale rules and bloated memory are technical debt this can surface alongside code debt | Memory health metrics feed debt scoring; debt prioritization informs which stale rules to retire |
| agent-workflow-designer | Multi-step workflows benefit from per-step feedback capture and cross-workflow pattern extraction | Per-step outcome data flows into feedback loops; extracted optimizations update workflow definitions |
Feedback Loop Patterns
Read this when designing how the agent captures outcomes, classifies feedback, detects performance regressions, and runs the learning/regression workflows. Includes the core improvement-loop architecture, the operational workflows, common pitfalls, troubleshooting, and the success-criteria bar.
Core Architecture
The Improvement Loop
┌──────────────────────────────────────────────────────────┐
│ SELF-IMPROVEMENT CYCLE │
│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Execute │───▶│ Evaluate │───▶│ Extract │ │
│ │ Task │ │ Outcome │ │ Learnings │ │
│ └─────────┘ └──────────┘ └─────────────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌─────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Apply │◀───│ Promote │◀───│ Validate │ │
│ │ Rules │ │ to Rules │ │ Learnings │ │
│ └─────────┘ └──────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘Improvement Maturity Levels
| Level | Name | Mechanism | Example |
|---|---|---|---|
| 0 | Stateless | No memory between sessions | Default agent behavior |
| 1 | Recording | Captures observations, no action | Auto-memory logging |
| 2 | Curating | Organizes and deduplicates observations | Memory review + cleanup |
| 3 | Promoting | Graduates patterns to enforced rules | MEMORY.md entries become CLAUDE.md rules |
| 4 | Extracting | Creates reusable skills from proven patterns | Recurring solutions become skill packages |
| 5 | Meta-Learning | Adapts learning strategy itself | Adjusts what to capture based on what proved useful |
Most agents operate at Level 0-1. This skill provides the machinery for Levels 2-5.
Feedback Loop Design
Outcome Classification
Every agent task produces an outcome. Classify it:
SUCCESS - Task completed, user accepted result
PARTIAL - Task completed but required corrections
FAILURE - Task failed, user had to redo
REJECTION - User explicitly rejected approach
TIMEOUT - Task exceeded time/token budget
ERROR - Technical error (tool failure, API error)Signal Extraction from Outcomes
| Outcome | Signal | Memory Action |
|---|---|---|
| SUCCESS (first try) | Approach works well | Reinforce (increment confidence) |
| SUCCESS (after correction) | Initial approach had gap | Log the correction pattern |
| PARTIAL (user edited result) | Output format or content gap | Log what user changed |
| FAILURE | Approach fundamentally wrong | Log anti-pattern with context |
| REJECTION | Misunderstood requirements | Log clarification pattern |
| Repeated ERROR | Tool or environment issue | Log workaround or fix |
Feedback Capture Template
## Learning: [Short description]
**Context:** [What task was being performed]
**What happened:** [Outcome description]
**Root cause:** [Why the outcome occurred]
**Correct approach:** [What should have been done]
**Confidence:** [High/Medium/Low]
**Recurrence:** [First time / Seen N times]
**Action:** [KEEP / PROMOTE / EXTRACT]Performance Regression Detection
Metrics to Track
| Metric | Measurement | Regression Signal |
|---|---|---|
| First-attempt success rate | Tasks accepted without correction | Dropping below 70% |
| Correction count per task | User edits after agent output | Rising above 2 per task |
| Tool error rate | Failed tool calls / total calls | Rising above 5% |
| Context relevance | Retrieved context actually used | Dropping below 60% |
| Task completion time | Turns to complete task | Rising trend over 5 sessions |
Regression Response Protocol
1. DETECT: Metric crosses threshold
2. DIAGNOSE: Compare recent sessions vs baseline
- What changed? (New code? New patterns? New tools?)
- Which task types are affected?
- Is it a memory issue or a capability issue?
3. RESPOND:
- Memory issue → Review and curate MEMORY.md
- Stale rules → Update CLAUDE.md
- New code patterns → Add rules for new patterns
- Capability gap → Extract as skill request
4. VERIFY: Track metric for next 3 sessionsWorkflows
Workflow 2: Post-Session Learning Capture
1. Review session outcomes (successes, corrections, failures)
2. For each correction: log what was wrong and what was right
3. For each failure: log root cause and correct approach
4. Check existing memory for related entries
5. If related entry exists: increment recurrence count
6. If new: add entry with context
7. If recurrence threshold met: flag for promotionWorkflow 3: Regression Investigation
1. Identify the degraded metric
2. Pull last 5 sessions' outcomes for that task type
3. Compare against baseline (first 5 sessions)
4. Identify what changed: memory, code, rules, environment
5. Propose fix: update rule, add rule, retrain pattern
6. Apply fix
7. Monitor next 3 sessionsCommon Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Memory bloat | Auto-capture without curation | Weekly review, enforce 200-line limit |
| Stale rules | Code changes, rules don't update | Timestamp rules, periodic re-verification |
| Over-promotion | Promoting one-off patterns as rules | Require 3+ recurrences before promotion |
| Silent regression | No metrics tracking | Implement outcome classification |
| Cargo cult rules | Copying rules without understanding | Each rule must have a "why" annotation |
| Contradiction spirals | New rules conflict with old rules | Belief revision protocol |
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| MEMORY.md exceeds 200 lines and keeps growing | Auto-capture enabled without scheduled curation | Run the Weekly Memory Health Check workflow; split topic-specific entries into memory/<topic>.md files |
| Promoted rules contradict each other | Two conflicting patterns both crossed the 3-recurrence threshold | Apply the Belief Revision protocol -- compare confidence scores, resolve the conflict, delete the weaker rule |
| Agent performance degrades after a promotion batch | Newly promoted rules interact badly or are overly prescriptive | Roll back the most recent promotions, re-validate each rule in isolation, and promote incrementally |
| Skill extraction produces a package that only works on the original project | Generalization step was skipped or rushed | Revisit Extraction Process Step 2 -- strip project-specific details, parameterize hardcoded values, test on a second project before packaging |
| Feedback loop captures noise (trivial observations dominate) | Capture strategy has not been calibrated with the Adaptive Capture Strategy | After 10 sessions, analyze promotion rates by category and restrict capture to high-value categories (error resolutions, user corrections, tool preferences) |
| Regression Detection flags false positives | Thresholds set too aggressively for early-stage projects | Widen thresholds during the first 20 sessions (e.g., first-attempt success 60% instead of 70%), then tighten once a stable baseline exists |
| Confidence scores decay too fast on valid long-term rules | Recency factor penalizes rules that are infrequently encountered but still correct | For rules explicitly confirmed by the user, override the recency factor to 1.0 regardless of age |
Success Criteria
- First-attempt success rate above 80% after 20 sessions of active self-improvement, measured as tasks accepted without user correction.
- Memory size stays under 200 lines in MEMORY.md at all times, with overflow correctly routed to topic files.
- Promotion rate of 15-25% of captured observations within 30 days, indicating the capture strategy targets high-value signals.
- Zero stale rules remaining after each Weekly Memory Health Check -- every rule references current code, tools, and workflows.
- Regression detection latency under 3 sessions -- performance degradation is flagged within 3 sessions of onset, not discovered weeks later.
- Extracted skills reusable across 2+ projects without modification, validating that the generalization step produces genuinely portable packages.
- Contradiction resolution within 1 session -- conflicting rules are detected and resolved via the Belief Revision protocol before they cause downstream errors.
Memory Curation Guide
Read this for step-by-step memory review and promotion procedures, and for the continuous calibration (confidence scoring + belief revision) machinery.
Memory Curation System
The Memory Stack
┌─────────────────────────────────────────────────┐
│ CLAUDE.md / .claude/rules/ │
│ Highest authority. Enforced every session. │
│ Capacity: Unlimited. Load: Full file. │
├─────────────────────────────────────────────────┤
│ MEMORY.md (auto-memory) │
│ Project learnings. Auto-captured by Claude. │
│ Capacity: First 200 lines loaded. Overflow to │
│ topic files. │
├─────────────────────────────────────────────────┤
│ Session Context │
│ Current conversation. Ephemeral. │
│ Capacity: Context window. │
└─────────────────────────────────────────────────┘Memory Review Protocol
Run periodically (weekly or after every 10 sessions):
Step 1: Read MEMORY.md and all topic files
Step 2: Classify each entry
Categories:
- PROMOTE: Pattern proven 3+ times, should be a rule
- CONSOLIDATE: Multiple entries saying the same thing
- STALE: References deleted files, old patterns, resolved issues
- KEEP: Still relevant, not yet proven enough to promote
- EXTRACT: Recurring solution that should be a reusable skill
Step 3: Execute actions
- PROMOTE entries → move to CLAUDE.md or .claude/rules/
- CONSOLIDATE entries → merge into single clear entry
- STALE entries → delete
- EXTRACT entries → create skill package (see Skill Extraction)
Step 4: Verify MEMORY.md is under 200 lines
- If over 200: move topic-specific entries to topic files
- Topic files: ~/.claude/projects/<path>/memory/<topic>.mdPromotion Criteria
An entry is ready for promotion when:
| Criterion | Threshold | Why |
|---|---|---|
| Recurrence | Seen in 3+ sessions | Not a one-off |
| Consistency | Same solution every time | Not context-dependent |
| Impact | Prevented errors or saved significant time | Worth enforcing |
| Stability | Underlying code/system unchanged | Won't immediately become stale |
| Clarity | Can be stated in 1-2 sentences | Rules must be unambiguous |
Promotion Targets
| Pattern Type | Promote To | Example |
|---|---|---|
| Coding convention | .claude/rules/<area>.md | "Always use type not interface for object shapes" |
| Project architecture | CLAUDE.md | "All API routes go through middleware chain" |
| Tool preference | CLAUDE.md | "Use pnpm, not npm" |
| Debugging pattern | .claude/rules/debugging.md | "When tests fail, check env vars first" |
| File-scoped rule | .claude/rules/<scope>.md with paths: | "In migrations/, always add down migration" |
Continuous Calibration
Confidence Scoring
Every piece of learned knowledge carries a confidence score:
Confidence = base_score * recency_factor * consistency_factor
base_score:
- User explicitly stated: 1.0
- Observed from successful outcome: 0.8
- Inferred from pattern: 0.6
- Guessed from context: 0.3
recency_factor:
- Last 7 days: 1.0
- 7-30 days: 0.9
- 30-90 days: 0.7
- 90+ days: 0.5
consistency_factor:
- Never contradicted: 1.0
- Contradicted once, reaffirmed: 0.9
- Contradicted, not reaffirmed: 0.5
- Actively contradicted: 0.0 (delete)Belief Revision
When new information contradicts existing knowledge:
1. Compare confidence scores
2. If new info higher confidence → update knowledge
3. If roughly equal → flag for user confirmation
4. If new info lower confidence → keep existing, note conflict
5. Always log the conflict for reviewWorkflows
Workflow 1: Weekly Memory Health Check
1. Read all memory files (MEMORY.md + topic files)
2. Count total entries and lines
3. For each entry, classify: PROMOTE / CONSOLIDATE / STALE / KEEP / EXTRACT
4. Execute promotions (with user confirmation)
5. Execute consolidations
6. Delete stale entries
7. Verify under 200-line limit
8. Report: entries promoted, consolidated, deleted, remainingMeta-Learning Architectures
Read this when designing agents that adapt their own learning strategy (what to capture and when) and that graduate proven patterns into standalone, reusable skills.
Skill Extraction
When a solution pattern is proven and reusable, extract it into a standalone skill.
Extraction Criteria
A pattern is ready for extraction when:
- Used successfully 5+ times across different contexts
- Solution is generalizable (not project-specific)
- Takes more than trivial effort to recreate from scratch
- Would benefit other projects/usersExtraction Process
Step 1: Document the pattern
- What problem does it solve?
- What's the step-by-step approach?
- What are the inputs and outputs?
- What are the edge cases?
Step 2: Generalize
- Remove project-specific details
- Identify configurable parameters
- Add handling for common variations
Step 3: Package as skill
- Create SKILL.md with frontmatter
- Add references/ for knowledge bases
- Add scripts/ if automatable
- Add assets/ for templates
Step 4: Validate
- Test on a different project
- Have another person/agent use it
- Iterate on unclear instructionsMeta-Learning Patterns
Adaptive Capture Strategy
Not all observations are equally valuable. Adjust what gets captured based on what proved useful:
Initial strategy: Capture everything
After 10 sessions: Analyze which captured items led to promotions
After 20 sessions: Adjust capture to focus on high-value categories
High-value categories (typically):
- Error resolutions (80% promotion rate)
- User corrections (70% promotion rate)
- Tool preferences (60% promotion rate)
Low-value categories (typically):
- File structure observations (10% promotion rate)
- One-off workarounds (5% promotion rate)Anti-Pattern Detection
Beyond capturing what works, actively detect what fails:
| Anti-Pattern | Detection Signal | Response |
|---|---|---|
| Repeated wrong import path | Same correction 3+ times | Add to CLAUDE.md as rule |
| Wrong test framework used | User always changes test approach | Add testing rules |
| Incorrect API usage | Same API error pattern | Add API usage notes |
| Style guide violations | User reformats same patterns | Add style rules |
| Wrong branch workflow | User corrects git operations | Add git workflow rules |
Self-Improvement Methodology Reference
The Five Layers of Agent Learning
Layer 1: Session Memory (Ephemeral)
- Context window contents
- Current conversation state
- Tool call history for this session
- Lost when session ends
Layer 2: Persistent Memory (MEMORY.md)
- Observations captured across sessions
- Key-value learnings with metadata
- 200-line limit to prevent bloat
- Topic files for overflow
Layer 3: Enforced Rules (CLAUDE.md / .claude/rules/)
- Promoted patterns that proved reliable
- Loaded every session automatically
- Highest authority after system instructions
- Must have "why" annotation
Layer 4: Extracted Skills
- Reusable packages graduated from patterns
- Self-contained with scripts and references
- Can be shared across projects
Layer 5: Meta-Learning
- Strategies for what to capture and when
- Adaptive thresholds based on value delivered
- Self-tuning promotion criteria
Confidence Scoring Model
effective_confidence = base_score * recency_factor * consistency_factor
base_score:
user-stated: 1.0 (user explicitly told us)
observed: 0.8 (we saw it work)
inferred: 0.6 (we deduced from evidence)
guessed: 0.3 (speculation)
recency_factor:
0-7 days: 1.0
7-30 days: 0.9
30-90 days: 0.7
90+ days: 0.5
consistency_factor:
never contradicted: 1.0
contradicted + reaffirmed: 0.9
contradicted, unresolved: 0.5
actively contradicted: 0.0 (delete)Promotion Decision Tree
Entry in MEMORY.md
├── Recurrence >= 3?
│ ├── NO → KEEP (continue monitoring)
│ └── YES
│ ├── Consistent solution every time?
│ │ ├── NO → KEEP (needs more evidence)
│ │ └── YES
│ │ ├── Referenced code/tools still exist?
│ │ │ ├── NO → STALE (delete)
│ │ │ └── YES
│ │ │ ├── Expressible in 1-2 sentences?
│ │ │ │ ├── NO → EXTRACT (make a skill)
│ │ │ │ └── YES → PROMOTE
│ │ │ │ ├── Coding convention → .claude/rules/
│ │ │ │ ├── Architecture rule → CLAUDE.md
│ │ │ │ ├── Tool preference → CLAUDE.md
│ │ │ │ └── Scoped rule → .claude/rules/ with paths:Memory Curation Checklist
Weekly health check steps:
1. [ ] Read MEMORY.md completely 2. [ ] Check line count (must be < 200) 3. [ ] For each entry, classify: PROMOTE / CONSOLIDATE / STALE / KEEP / EXTRACT 4. [ ] Merge duplicates (CONSOLIDATE) 5. [ ] Delete stale entries (references deleted code, old patterns) 6. [ ] Promote ready entries (recurrence >= 3, consistent, impactful) 7. [ ] Move topic-specific overflow to topic files 8. [ ] Verify all rules in .claude/rules/ still reference existing code 9. [ ] Log the health check outcome
Anti-Patterns in Self-Improvement
| Anti-Pattern | Symptom | Fix |
|---|---|---|
| Memory hoarding | MEMORY.md > 200 lines, never pruned | Schedule weekly curation |
| Premature promotion | Rules promoted after 1 occurrence | Enforce 3+ recurrence minimum |
| Cargo cult rules | Rules copied without understanding why | Require "why" annotation on every rule |
| Stale rule accumulation | Rules reference deleted code/tools | Timestamp rules, verify periodically |
| Contradiction spiral | New rules conflict with existing ones | Belief revision protocol: compare confidence, resolve |
| Observation bias | Only capturing failures, not successes | Track both; success patterns inform approach selection |
| Over-promotion | Everything becomes a rule | Cap at 15-25% promotion rate; most entries should KEEP |
Metrics and Thresholds
| Metric | Healthy Range | Action if Out of Range |
|---|---|---|
| MEMORY.md line count | 50-200 | Prune if over; if under 50, capture more |
| Promotion rate (30d) | 15-25% | Adjust capture strategy or promotion criteria |
| Stale entry ratio | < 10% | Run curation immediately |
| Rule count | 10-50 | Consolidate if over 50; add more if under 10 |
| First-attempt success | > 70% | Investigate regressions, check rule quality |
| Contradiction count | 0 | Resolve immediately via belief revision |
#!/usr/bin/env python3
"""Feedback Analyzer - Analyze feedback logs to extract patterns and improvement opportunities.
Reads feedback log files (JSON or JSONL format) containing agent session outcomes,
and produces analysis of success rates, failure patterns, and actionable improvement
recommendations based on the Self-Improving Agent feedback loop model.
Expected log entry format:
{
"session_id": "abc123",
"timestamp": "2026-03-15T10:30:00",
"task_type": "code-review",
"outcome": "SUCCESS|PARTIAL|FAILURE|REJECTION|TIMEOUT|ERROR",
"corrections": 0,
"turns": 5,
"tools_used": ["Read", "Edit", "Bash"],
"tool_errors": 0,
"notes": "optional description"
}
Usage:
python feedback_analyzer.py analyze --input feedback.jsonl
python feedback_analyzer.py patterns --input feedback.jsonl --min-count 3
python feedback_analyzer.py trends --input feedback.jsonl --window 7
python feedback_analyzer.py opportunities --input feedback.jsonl
"""
import argparse
import json
import os
import sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from pathlib import Path
VALID_OUTCOMES = ["SUCCESS", "PARTIAL", "FAILURE", "REJECTION", "TIMEOUT", "ERROR"]
THRESHOLDS = {
"first_attempt_success": 0.70,
"corrections_per_task": 2.0,
"tool_error_rate": 0.05,
"completion_turns": 10,
}
def load_feedback(path: str) -> list:
"""Load feedback entries from JSON or JSONL file."""
entries = []
with open(path, "r") as f:
content = f.read().strip()
if content.startswith("["):
entries = json.loads(content)
else:
for line in content.splitlines():
line = line.strip()
if line:
entries.append(json.loads(line))
for e in entries:
if "timestamp" in e:
e["_dt"] = datetime.fromisoformat(e["timestamp"])
return entries
def cmd_analyze(args, entries: list) -> dict:
"""Produce summary statistics from feedback entries."""
if not entries:
return {"action": "analyze", "error": "No entries found"}
total = len(entries)
outcome_counts = Counter(e.get("outcome", "UNKNOWN") for e in entries)
task_type_counts = Counter(e.get("task_type", "unknown") for e in entries)
successes = outcome_counts.get("SUCCESS", 0)
first_attempt = sum(1 for e in entries if e.get("outcome") == "SUCCESS" and e.get("corrections", 0) == 0)
corrections = [e.get("corrections", 0) for e in entries]
avg_corrections = sum(corrections) / total if total else 0
turns = [e.get("turns", 0) for e in entries]
avg_turns = sum(turns) / total if total else 0
total_tool_calls = sum(len(e.get("tools_used", [])) for e in entries)
total_tool_errors = sum(e.get("tool_errors", 0) for e in entries)
tool_error_rate = total_tool_errors / total_tool_calls if total_tool_calls else 0
# Per-task-type breakdown
task_breakdown = {}
for task_type in task_type_counts:
task_entries = [e for e in entries if e.get("task_type") == task_type]
t_total = len(task_entries)
t_success = sum(1 for e in task_entries if e.get("outcome") == "SUCCESS")
t_first = sum(1 for e in task_entries if e.get("outcome") == "SUCCESS" and e.get("corrections", 0) == 0)
task_breakdown[task_type] = {
"total": t_total,
"success_rate": round(t_success / t_total, 3) if t_total else 0,
"first_attempt_rate": round(t_first / t_total, 3) if t_total else 0,
"avg_corrections": round(sum(e.get("corrections", 0) for e in task_entries) / t_total, 2),
}
# Health assessment
health_flags = []
fa_rate = first_attempt / total if total else 0
if fa_rate < THRESHOLDS["first_attempt_success"]:
health_flags.append(f"First-attempt success rate {fa_rate:.1%} below {THRESHOLDS['first_attempt_success']:.0%} threshold")
if avg_corrections > THRESHOLDS["corrections_per_task"]:
health_flags.append(f"Avg corrections {avg_corrections:.1f} exceeds {THRESHOLDS['corrections_per_task']} threshold")
if tool_error_rate > THRESHOLDS["tool_error_rate"]:
health_flags.append(f"Tool error rate {tool_error_rate:.1%} exceeds {THRESHOLDS['tool_error_rate']:.0%} threshold")
return {
"action": "analyze",
"total_entries": total,
"outcome_distribution": dict(outcome_counts),
"success_rate": round(successes / total, 3),
"first_attempt_success_rate": round(fa_rate, 3),
"avg_corrections": round(avg_corrections, 2),
"avg_turns": round(avg_turns, 2),
"tool_error_rate": round(tool_error_rate, 4),
"task_breakdown": task_breakdown,
"health_flags": health_flags,
}
def cmd_patterns(args, entries: list) -> dict:
"""Extract recurring patterns from feedback data."""
min_count = args.min_count
# Failure patterns by task type
failure_patterns = defaultdict(list)
for e in entries:
if e.get("outcome") in ("FAILURE", "REJECTION", "ERROR"):
failure_patterns[e.get("task_type", "unknown")].append({
"outcome": e["outcome"],
"notes": e.get("notes", ""),
"corrections": e.get("corrections", 0),
"session_id": e.get("session_id", ""),
})
recurring_failures = {
k: v for k, v in failure_patterns.items() if len(v) >= min_count
}
# Correction patterns -- what task types need most corrections
correction_patterns = defaultdict(list)
for e in entries:
if e.get("corrections", 0) > 0:
correction_patterns[e.get("task_type", "unknown")].append(e.get("corrections", 0))
high_correction_tasks = {}
for task_type, corr_list in correction_patterns.items():
if len(corr_list) >= min_count:
high_correction_tasks[task_type] = {
"occurrences": len(corr_list),
"avg_corrections": round(sum(corr_list) / len(corr_list), 2),
"max_corrections": max(corr_list),
}
# Tool error patterns
tool_error_patterns = defaultdict(int)
for e in entries:
if e.get("tool_errors", 0) > 0:
for tool in e.get("tools_used", []):
tool_error_patterns[tool] += 1
# Success patterns -- what works well
success_patterns = defaultdict(int)
for e in entries:
if e.get("outcome") == "SUCCESS" and e.get("corrections", 0) == 0:
success_patterns[e.get("task_type", "unknown")] += 1
return {
"action": "patterns",
"min_count_threshold": min_count,
"recurring_failures": recurring_failures,
"high_correction_tasks": high_correction_tasks,
"tool_error_frequency": dict(tool_error_patterns),
"strong_success_areas": dict(success_patterns),
}
def cmd_trends(args, entries: list) -> dict:
"""Analyze trends over time using a sliding window."""
window_days = args.window
if not entries or "_dt" not in entries[0]:
return {"action": "trends", "error": "No timestamped entries found"}
sorted_entries = sorted(entries, key=lambda e: e["_dt"])
start = sorted_entries[0]["_dt"]
end = sorted_entries[-1]["_dt"]
windows = []
current = start
while current <= end:
window_end = current + timedelta(days=window_days)
window_entries = [e for e in sorted_entries if current <= e["_dt"] < window_end]
if window_entries:
total = len(window_entries)
successes = sum(1 for e in window_entries if e.get("outcome") == "SUCCESS")
first_att = sum(1 for e in window_entries if e.get("outcome") == "SUCCESS" and e.get("corrections", 0) == 0)
avg_corr = sum(e.get("corrections", 0) for e in window_entries) / total
avg_turns = sum(e.get("turns", 0) for e in window_entries) / total
windows.append({
"period_start": current.isoformat(),
"period_end": window_end.isoformat(),
"entries": total,
"success_rate": round(successes / total, 3),
"first_attempt_rate": round(first_att / total, 3),
"avg_corrections": round(avg_corr, 2),
"avg_turns": round(avg_turns, 2),
})
current = window_end
# Compute trend direction
trend_signals = []
if len(windows) >= 2:
first_half = windows[: len(windows) // 2]
second_half = windows[len(windows) // 2 :]
fa_first = sum(w["first_attempt_rate"] for w in first_half) / len(first_half)
fa_second = sum(w["first_attempt_rate"] for w in second_half) / len(second_half)
delta = fa_second - fa_first
if delta > 0.05:
trend_signals.append(f"IMPROVING: First-attempt success up {delta:+.1%}")
elif delta < -0.05:
trend_signals.append(f"DEGRADING: First-attempt success down {delta:+.1%}")
else:
trend_signals.append("STABLE: First-attempt success rate unchanged")
corr_first = sum(w["avg_corrections"] for w in first_half) / len(first_half)
corr_second = sum(w["avg_corrections"] for w in second_half) / len(second_half)
corr_delta = corr_second - corr_first
if corr_delta > 0.3:
trend_signals.append(f"WARNING: Avg corrections increasing ({corr_delta:+.2f})")
elif corr_delta < -0.3:
trend_signals.append(f"IMPROVING: Avg corrections decreasing ({corr_delta:+.2f})")
return {
"action": "trends",
"window_days": window_days,
"total_periods": len(windows),
"windows": windows,
"trend_signals": trend_signals,
}
def cmd_opportunities(args, entries: list) -> dict:
"""Identify concrete improvement opportunities from feedback data."""
opportunities = []
# Opportunity 1: Task types with low success rates
task_outcomes = defaultdict(lambda: {"total": 0, "success": 0, "failures": []})
for e in entries:
tt = e.get("task_type", "unknown")
task_outcomes[tt]["total"] += 1
if e.get("outcome") == "SUCCESS":
task_outcomes[tt]["success"] += 1
elif e.get("outcome") in ("FAILURE", "REJECTION"):
task_outcomes[tt]["failures"].append(e.get("notes", ""))
for tt, data in task_outcomes.items():
rate = data["success"] / data["total"] if data["total"] else 0
if rate < 0.6 and data["total"] >= 3:
opportunities.append({
"type": "low_success_task",
"priority": "high",
"task_type": tt,
"success_rate": round(rate, 3),
"sample_count": data["total"],
"recommendation": f"Add dedicated rules for '{tt}' tasks -- success rate is {rate:.0%}",
"failure_notes": [n for n in data["failures"] if n][:3],
})
# Opportunity 2: High correction tasks that could benefit from rules
for e in entries:
tt = e.get("task_type", "unknown")
corr_by_type = defaultdict(list)
for e in entries:
corr_by_type[e.get("task_type", "unknown")].append(e.get("corrections", 0))
for tt, corrs in corr_by_type.items():
avg = sum(corrs) / len(corrs)
if avg > 1.5 and len(corrs) >= 3:
opportunities.append({
"type": "high_correction_task",
"priority": "medium",
"task_type": tt,
"avg_corrections": round(avg, 2),
"recommendation": f"Capture correction patterns for '{tt}' and promote to rules",
})
# Opportunity 3: Tool reliability issues
tool_stats = defaultdict(lambda: {"uses": 0, "errors": 0})
for e in entries:
for tool in e.get("tools_used", []):
tool_stats[tool]["uses"] += 1
if e.get("tool_errors", 0) > 0:
for tool in e.get("tools_used", []):
tool_stats[tool]["errors"] += 1
for tool, stats in tool_stats.items():
err_rate = stats["errors"] / stats["uses"] if stats["uses"] else 0
if err_rate > 0.1 and stats["uses"] >= 5:
opportunities.append({
"type": "tool_reliability",
"priority": "medium",
"tool": tool,
"error_rate": round(err_rate, 3),
"uses": stats["uses"],
"recommendation": f"Investigate '{tool}' reliability -- {err_rate:.0%} error rate",
})
# Opportunity 4: Timeout tasks suggesting scope issues
timeout_count = sum(1 for e in entries if e.get("outcome") == "TIMEOUT")
if timeout_count >= 2:
opportunities.append({
"type": "timeout_pattern",
"priority": "high",
"count": timeout_count,
"recommendation": "Review task scoping -- multiple timeouts suggest tasks are too large or under-specified",
})
opportunities.sort(key=lambda o: {"high": 0, "medium": 1, "low": 2}.get(o["priority"], 3))
return {"action": "opportunities", "count": len(opportunities), "opportunities": opportunities}
def format_human(result: dict) -> str:
"""Format result for human-readable output."""
action = result.get("action", "unknown")
lines = []
if "error" in result:
return f"Error: {result['error']}"
if action == "analyze":
lines.append(f"Feedback Analysis ({result['total_entries']} entries)")
lines.append("=" * 50)
lines.append(f" Success rate: {result['success_rate']:.1%}")
lines.append(f" First-attempt success: {result['first_attempt_success_rate']:.1%}")
lines.append(f" Avg corrections/task: {result['avg_corrections']:.2f}")
lines.append(f" Avg turns/task: {result['avg_turns']:.1f}")
lines.append(f" Tool error rate: {result['tool_error_rate']:.2%}")
lines.append("")
lines.append("Outcome Distribution:")
for outcome, count in sorted(result["outcome_distribution"].items()):
bar = "#" * min(count, 40)
lines.append(f" {outcome:<12} {count:>4} {bar}")
lines.append("")
lines.append("Per-Task Breakdown:")
for tt, stats in sorted(result["task_breakdown"].items()):
lines.append(f" {tt}: {stats['total']} tasks, {stats['success_rate']:.0%} success, {stats['avg_corrections']:.1f} avg corrections")
if result["health_flags"]:
lines.append("")
lines.append("Health Warnings:")
for flag in result["health_flags"]:
lines.append(f" [!] {flag}")
elif action == "patterns":
lines.append("Feedback Patterns")
lines.append("=" * 50)
if result["recurring_failures"]:
lines.append("Recurring Failures:")
for tt, failures in result["recurring_failures"].items():
lines.append(f" {tt}: {len(failures)} occurrences")
if result["high_correction_tasks"]:
lines.append("High-Correction Tasks:")
for tt, stats in result["high_correction_tasks"].items():
lines.append(f" {tt}: avg {stats['avg_corrections']} corrections ({stats['occurrences']} occurrences)")
if result["strong_success_areas"]:
lines.append("Strong Areas (first-attempt success):")
for tt, count in sorted(result["strong_success_areas"].items(), key=lambda x: -x[1]):
lines.append(f" {tt}: {count} first-attempt successes")
elif action == "trends":
lines.append(f"Trend Analysis ({result['window_days']}-day windows)")
lines.append("=" * 50)
for w in result["windows"]:
lines.append(f" {w['period_start'][:10]} - {w['period_end'][:10]}: "
f"{w['entries']} entries, {w['success_rate']:.0%} success, "
f"{w['avg_corrections']:.1f} avg corrections")
if result["trend_signals"]:
lines.append("")
for signal in result["trend_signals"]:
lines.append(f" >> {signal}")
elif action == "opportunities":
lines.append(f"Improvement Opportunities ({result['count']} found)")
lines.append("=" * 50)
for i, opp in enumerate(result["opportunities"], 1):
lines.append(f"\n {i}. [{opp['priority'].upper()}] {opp['type']}")
lines.append(f" {opp['recommendation']}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Feedback Analyzer - Extract patterns and opportunities from agent feedback logs",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--json", action="store_true", dest="json_output", help="Output in JSON format")
sub = parser.add_subparsers(dest="command", help="Available commands")
p_analyze = sub.add_parser("analyze", help="Summary statistics of feedback data")
p_analyze.add_argument("--input", required=True, help="Path to feedback log file (JSON or JSONL)")
p_patterns = sub.add_parser("patterns", help="Extract recurring patterns")
p_patterns.add_argument("--input", required=True, help="Path to feedback log file")
p_patterns.add_argument("--min-count", type=int, default=3, help="Minimum occurrences to flag a pattern")
p_trends = sub.add_parser("trends", help="Analyze trends over time")
p_trends.add_argument("--input", required=True, help="Path to feedback log file")
p_trends.add_argument("--window", type=int, default=7, help="Window size in days")
p_opp = sub.add_parser("opportunities", help="Identify improvement opportunities")
p_opp.add_argument("--input", required=True, help="Path to feedback log file")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
if not os.path.exists(args.input):
print(f"Error: Input file '{args.input}' not found", file=sys.stderr)
sys.exit(1)
entries = load_feedback(args.input)
commands = {
"analyze": cmd_analyze,
"patterns": cmd_patterns,
"trends": cmd_trends,
"opportunities": cmd_opportunities,
}
result = commands[args.command](args, entries)
# Remove internal fields before output
if "rules" in result or "windows" in result or "opportunities" in result:
pass # keep structured data
for entry_list_key in ["recurring_failures"]:
if entry_list_key in result:
for key, val_list in result[entry_list_key].items():
for v in val_list:
v.pop("_dt", None)
if args.json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(format_human(result))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Check memory system health: line counts, stale entries, contradictions.
Scans MEMORY.md and related files for health issues including line limit
violations, stale references, duplicate entries, and promotion candidates.
Produces an actionable health report.
Usage:
python memory_health_checker.py --memory ./MEMORY.md
python memory_health_checker.py --memory ./MEMORY.md --rules ./.claude/rules/
python memory_health_checker.py --memory ./MEMORY.md --json
"""
import argparse
import json
import os
import re
import sys
from collections import defaultdict
from datetime import datetime, timedelta
from pathlib import Path
MEMORY_LINE_LIMIT = 200
TOPIC_FILE_LINE_LIMIT = 100
STALE_AGE_DAYS = 90
PROMOTION_THRESHOLD = 3
def load_memory_file(path):
"""Load and parse a memory file into structured entries."""
if not os.path.exists(path):
return {"path": path, "exists": False, "lines": 0, "entries": []}
content = Path(path).read_text(encoding="utf-8")
lines = content.split("\n")
entries = []
current_entry = None
for i, line in enumerate(lines, 1):
# Detect entry headers (## Learning: or ## heading)
if line.startswith("## "):
if current_entry:
current_entry["end_line"] = i - 1
entries.append(current_entry)
current_entry = {
"title": line.lstrip("# ").strip(),
"start_line": i,
"end_line": None,
"content_lines": [],
"metadata": {},
}
elif current_entry:
current_entry["content_lines"].append(line)
# Extract metadata from key-value lines
kv_match = re.match(r"\*\*(\w[\w\s]*)\*\*:\s*(.+)", line)
if kv_match:
key = kv_match.group(1).strip().lower()
value = kv_match.group(2).strip()
current_entry["metadata"][key] = value
if current_entry:
current_entry["end_line"] = len(lines)
entries.append(current_entry)
return {
"path": str(path),
"exists": True,
"lines": len(lines),
"entries": entries,
}
def load_rules(rules_dir):
"""Load existing promoted rules for comparison."""
rules = []
rules_path = Path(rules_dir)
if not rules_path.exists():
return rules
for rule_file in rules_path.glob("*.md"):
content = rule_file.read_text(encoding="utf-8")
rules.append({
"file": str(rule_file),
"content": content,
"tokens": set(re.findall(r"[a-zA-Z_]{3,}", content.lower())),
})
return rules
def classify_entry(entry, existing_rules, all_entries):
"""Classify a memory entry into PROMOTE, CONSOLIDATE, STALE, KEEP, or EXTRACT."""
title = entry["title"].lower()
metadata = entry["metadata"]
content_text = " ".join(entry["content_lines"]).lower()
entry_tokens = set(re.findall(r"[a-zA-Z_]{3,}", content_text))
issues = []
classification = "KEEP"
# Check recurrence for promotion
recurrence_str = metadata.get("recurrence", "")
recurrence_count = 0
match = re.search(r"(\d+)", recurrence_str)
if match:
recurrence_count = int(match.group(1))
if recurrence_count >= PROMOTION_THRESHOLD:
classification = "PROMOTE"
issues.append(f"Recurrence {recurrence_count} meets promotion threshold ({PROMOTION_THRESHOLD})")
# Check for explicit action metadata
action = metadata.get("action", "").upper()
if action == "PROMOTE":
classification = "PROMOTE"
elif action == "EXTRACT":
classification = "EXTRACT"
# Check for stale entries (references to deleted files, old dates)
confidence = metadata.get("confidence", "").lower()
if confidence == "low":
issues.append("Low confidence entry")
if classification == "KEEP":
classification = "STALE"
# Check for duplicates / consolidation candidates
for other in all_entries:
if other is entry:
continue
other_tokens = set(re.findall(r"[a-zA-Z_]{3,}", " ".join(other["content_lines"]).lower()))
if entry_tokens and other_tokens:
overlap = len(entry_tokens & other_tokens) / max(len(entry_tokens | other_tokens), 1)
if overlap > 0.6:
if classification in ("KEEP", "STALE"):
classification = "CONSOLIDATE"
issues.append(f"Similar to entry: {other['title'][:40]}")
break
# Check if already promoted (redundant in memory)
for rule in existing_rules:
overlap = len(entry_tokens & rule["tokens"]) / max(len(entry_tokens | rule["tokens"]), 1)
if overlap > 0.5:
classification = "STALE"
issues.append(f"Already promoted to: {rule['file']}")
break
return {
"title": entry["title"],
"start_line": entry["start_line"],
"classification": classification,
"recurrence": recurrence_count,
"issues": issues,
}
def check_constraints(memory_data, topic_files):
"""Check system-wide constraints."""
violations = []
if memory_data["lines"] > MEMORY_LINE_LIMIT:
violations.append({
"type": "line_limit",
"file": memory_data["path"],
"current": memory_data["lines"],
"limit": MEMORY_LINE_LIMIT,
"severity": "high",
})
for tf in topic_files:
if tf["lines"] > TOPIC_FILE_LINE_LIMIT:
violations.append({
"type": "line_limit",
"file": tf["path"],
"current": tf["lines"],
"limit": TOPIC_FILE_LINE_LIMIT,
"severity": "medium",
})
return violations
def run_health_check(memory_path, rules_dir, topic_dir):
"""Run complete health check."""
memory_data = load_memory_file(memory_path)
if not memory_data["exists"]:
return {
"status": "NO_MEMORY",
"message": f"No memory file found at {memory_path}",
"maturity_level": 0,
}
# Load topic files
topic_files = []
if topic_dir and os.path.isdir(topic_dir):
for tf in Path(topic_dir).glob("*.md"):
topic_files.append(load_memory_file(tf))
# Load rules
existing_rules = load_rules(rules_dir) if rules_dir else []
# Classify entries
classifications = []
for entry in memory_data["entries"]:
cls = classify_entry(entry, existing_rules, memory_data["entries"])
classifications.append(cls)
# Count by classification
counts = defaultdict(int)
for c in classifications:
counts[c["classification"]] += 1
# Check constraints
violations = check_constraints(memory_data, topic_files)
# Determine health status
if violations and any(v["severity"] == "high" for v in violations):
health_status = "CRITICAL"
elif counts["STALE"] > 5 or counts["CONSOLIDATE"] > 5:
health_status = "NEEDS_ATTENTION"
elif counts["PROMOTE"] > 0:
health_status = "GOOD_WITH_ACTIONS"
else:
health_status = "HEALTHY"
# Determine maturity level
if not memory_data["entries"]:
maturity = 1 # Recording (file exists but empty-ish)
elif counts["PROMOTE"] > 0 or len(existing_rules) > 0:
maturity = 3 # Promoting
elif counts["CONSOLIDATE"] > 0 or counts["STALE"] > 0:
maturity = 2 # Curating
else:
maturity = 1 # Recording
return {
"status": health_status,
"maturity_level": maturity,
"memory_file": {
"path": memory_data["path"],
"lines": memory_data["lines"],
"line_limit": MEMORY_LINE_LIMIT,
"entry_count": len(memory_data["entries"]),
},
"topic_files": [{"path": tf["path"], "lines": tf["lines"]} for tf in topic_files],
"classifications": classifications,
"counts": dict(counts),
"constraint_violations": violations,
"existing_rules": len(existing_rules),
}
def format_human(result):
"""Format health check result for human output."""
output = []
output.append("=" * 60)
output.append("MEMORY HEALTH CHECK")
output.append("=" * 60)
if result["status"] == "NO_MEMORY":
output.append(f" {result['message']}")
output.append(f" Maturity Level: 0 (Stateless)")
return "\n".join(output)
mf = result["memory_file"]
maturity_names = {0: "Stateless", 1: "Recording", 2: "Curating", 3: "Promoting", 4: "Extracting", 5: "Meta-Learning"}
output.append(f" Status: {result['status']}")
output.append(f" Maturity Level: {result['maturity_level']} ({maturity_names.get(result['maturity_level'], '?')})")
output.append(f" Memory file: {mf['path']}")
output.append(f" Lines: {mf['lines']}/{mf['line_limit']}")
output.append(f" Entries: {mf['entry_count']}")
output.append(f" Existing rules: {result['existing_rules']}")
output.append("")
# Classification summary
output.append("ENTRY CLASSIFICATIONS")
output.append("-" * 60)
counts = result["counts"]
order = ["PROMOTE", "CONSOLIDATE", "STALE", "KEEP", "EXTRACT", "CONTRADICTION"]
for cls in order:
count = counts.get(cls, 0)
if count > 0:
marker = {"PROMOTE": ">>", "STALE": "xx", "CONSOLIDATE": "==", "EXTRACT": "->", "KEEP": " "}.get(cls, " ")
output.append(f" {marker} {cls:<15} {count}")
output.append("")
# Detailed classifications
actionable = [c for c in result["classifications"] if c["classification"] != "KEEP"]
if actionable:
output.append("ACTIONABLE ENTRIES")
output.append("-" * 60)
for c in actionable:
output.append(f" [{c['classification']}] Line {c['start_line']}: {c['title'][:50]}")
for issue in c["issues"]:
output.append(f" - {issue}")
output.append("")
# Constraint violations
if result["constraint_violations"]:
output.append("CONSTRAINT VIOLATIONS")
output.append("-" * 60)
for v in result["constraint_violations"]:
output.append(f" [{v['severity'].upper()}] {v['file']}: {v['current']} lines (limit: {v['limit']})")
output.append("")
return "\n".join(output)
def main():
parser = argparse.ArgumentParser(
description="Check memory system health: line counts, stale entries, contradictions.",
epilog="Example: python memory_health_checker.py --memory ./MEMORY.md --rules ./.claude/rules/",
)
parser.add_argument("--memory", required=True, help="Path to MEMORY.md file")
parser.add_argument("--rules", default=None, help="Path to rules directory (.claude/rules/)")
parser.add_argument("--topics", default=None, help="Path to topic memory files directory")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
args = parser.parse_args()
result = run_health_check(args.memory, args.rules, args.topics)
if args.json_output:
print(json.dumps(result, indent=2))
else:
print(format_human(result))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Extract reusable patterns from agent session logs.
Analyzes JSONL session logs to identify recurring approaches, error resolutions,
and workflow patterns. Scores each pattern by frequency, consistency, and impact
to recommend which patterns should be promoted to rules.
Expected log entry format (JSONL):
{"session_id": "s1", "task_type": "code-review", "outcome": "SUCCESS",
"approach": "used page objects", "corrections": 0, "error_resolved": "",
"tools_used": ["Read", "Edit"], "notes": ""}
Usage:
python pattern_extractor.py --input sessions.jsonl --min-occurrences 2
python pattern_extractor.py --input sessions.jsonl --min-occurrences 3 --json
python pattern_extractor.py --input sessions.jsonl --days 30
"""
import argparse
import json
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from pathlib import Path
def load_sessions(path, max_days=None):
"""Load session log entries from JSONL file."""
entries = []
with open(path, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
if "timestamp" in entry:
entry["_dt"] = datetime.fromisoformat(entry["timestamp"])
entries.append(entry)
except json.JSONDecodeError:
continue
if max_days and entries:
cutoff = datetime.now() - timedelta(days=max_days)
entries = [e for e in entries if e.get("_dt", datetime.max) >= cutoff]
return entries
def tokenize(text):
"""Extract meaningful tokens from text for similarity matching."""
if not text:
return set()
words = re.findall(r"[a-zA-Z_][a-zA-Z0-9_]{2,}", text.lower())
stop_words = {"the", "and", "for", "with", "from", "that", "this", "was", "used", "using"}
return set(words) - stop_words
def compute_similarity(tokens_a, tokens_b):
"""Jaccard similarity between two token sets."""
if not tokens_a or not tokens_b:
return 0.0
intersection = tokens_a & tokens_b
union = tokens_a | tokens_b
return len(intersection) / len(union)
def cluster_approaches(entries, similarity_threshold=0.4):
"""Cluster similar approaches into pattern groups."""
# Extract approach descriptions
approaches = []
for e in entries:
approach_text = e.get("approach", "") or e.get("notes", "")
if approach_text:
approaches.append({
"text": approach_text,
"tokens": tokenize(approach_text),
"entry": e,
})
if not approaches:
return []
# Simple greedy clustering
clusters = []
assigned = set()
for i, a in enumerate(approaches):
if i in assigned:
continue
cluster = [a]
assigned.add(i)
for j, b in enumerate(approaches):
if j in assigned:
continue
sim = compute_similarity(a["tokens"], b["tokens"])
if sim >= similarity_threshold:
cluster.append(b)
assigned.add(j)
if len(cluster) >= 1:
clusters.append(cluster)
return clusters
def extract_error_patterns(entries, min_count=2):
"""Extract recurring error resolution patterns."""
error_resolutions = defaultdict(list)
for e in entries:
error = e.get("error_resolved", "")
if error:
error_tokens = frozenset(tokenize(error))
if error_tokens:
error_resolutions[error_tokens].append({
"error": error,
"task_type": e.get("task_type", "unknown"),
"outcome": e.get("outcome", "unknown"),
"session_id": e.get("session_id", ""),
})
# Filter by minimum count
recurring = {}
for tokens, resolutions in error_resolutions.items():
if len(resolutions) >= min_count:
# Use the first error text as representative
recurring[resolutions[0]["error"]] = {
"count": len(resolutions),
"task_types": list(set(r["task_type"] for r in resolutions)),
"success_rate": sum(1 for r in resolutions if r["outcome"] == "SUCCESS") / len(resolutions),
}
return recurring
def extract_tool_patterns(entries, min_count=2):
"""Extract recurring tool usage sequences."""
tool_sequences = Counter()
for e in entries:
tools = e.get("tools_used", [])
if len(tools) >= 2:
# Use ordered pairs as a simple sequence fingerprint
for i in range(len(tools) - 1):
pair = f"{tools[i]} -> {tools[i + 1]}"
tool_sequences[pair] += 1
return {seq: count for seq, count in tool_sequences.items() if count >= min_count}
def score_pattern(cluster):
"""Score a pattern cluster on frequency, consistency, and impact."""
entries = [item["entry"] for item in cluster]
count = len(entries)
# Frequency score (0-1)
if count >= 7:
frequency = 1.0
elif count >= 4:
frequency = 0.7
elif count >= 2:
frequency = 0.4
else:
frequency = 0.1
# Consistency score: what fraction had the same outcome
outcomes = [e.get("outcome", "unknown") for e in entries]
most_common_outcome = Counter(outcomes).most_common(1)[0]
consistency = most_common_outcome[1] / len(outcomes) if outcomes else 0
# Impact score: based on outcome quality
successes = sum(1 for e in entries if e.get("outcome") == "SUCCESS")
zero_corrections = sum(1 for e in entries if e.get("corrections", 0) == 0 and e.get("outcome") == "SUCCESS")
impact = zero_corrections / count if count > 0 else 0
composite = round(frequency * 0.3 + consistency * 0.4 + impact * 0.3, 3)
return {
"frequency": round(frequency, 3),
"consistency": round(consistency, 3),
"impact": round(impact, 3),
"composite": composite,
}
def determine_recommendation(score, count):
"""Determine recommended action based on score."""
if score["composite"] >= 0.7 and count >= 3:
return "PROMOTE"
elif score["composite"] >= 0.5 and count >= 5:
return "PROMOTE"
elif score["composite"] >= 0.4:
return "KEEP"
else:
return "MONITOR"
def extract_patterns(entries, min_occurrences):
"""Main pattern extraction pipeline."""
clusters = cluster_approaches(entries, similarity_threshold=0.35)
error_patterns = extract_error_patterns(entries, min_occurrences)
tool_patterns = extract_tool_patterns(entries, min_occurrences)
# Score and rank approach patterns
approach_patterns = []
for cluster in clusters:
if len(cluster) < min_occurrences:
continue
score = score_pattern(cluster)
representative = cluster[0]["text"]
recommendation = determine_recommendation(score, len(cluster))
task_types = list(set(item["entry"].get("task_type", "unknown") for item in cluster))
approach_patterns.append({
"pattern": representative,
"occurrences": len(cluster),
"task_types": task_types,
"scores": score,
"recommendation": recommendation,
})
approach_patterns.sort(key=lambda p: -p["scores"]["composite"])
return {
"approach_patterns": approach_patterns,
"error_patterns": error_patterns,
"tool_patterns": tool_patterns,
}
def format_human(results, total_entries):
"""Format results for human-readable output."""
output = []
output.append("=" * 60)
output.append("PATTERN EXTRACTOR")
output.append("=" * 60)
output.append(f" Sessions analyzed: {total_entries}")
output.append(f" Approach patterns: {len(results['approach_patterns'])}")
output.append(f" Error patterns: {len(results['error_patterns'])}")
output.append(f" Tool patterns: {len(results['tool_patterns'])}")
output.append("")
if results["approach_patterns"]:
output.append("APPROACH PATTERNS (ranked by composite score)")
output.append("-" * 60)
for i, p in enumerate(results["approach_patterns"], 1):
rec_marker = {"PROMOTE": ">>", "KEEP": " ", "MONITOR": ".."}
marker = rec_marker.get(p["recommendation"], " ")
output.append(f" {marker} {i}. [{p['recommendation']}] (score={p['scores']['composite']:.2f}, n={p['occurrences']})")
output.append(f" {p['pattern'][:70]}")
output.append(f" Tasks: {', '.join(p['task_types'][:4])}")
output.append("")
if results["error_patterns"]:
output.append("ERROR RESOLUTION PATTERNS")
output.append("-" * 60)
for error, data in sorted(results["error_patterns"].items(), key=lambda x: -x[1]["count"]):
output.append(f" [{data['count']}x] {error[:60]}")
output.append(f" Success rate: {data['success_rate']:.0%} | Tasks: {', '.join(data['task_types'][:3])}")
output.append("")
if results["tool_patterns"]:
output.append("TOOL SEQUENCE PATTERNS")
output.append("-" * 60)
for seq, count in sorted(results["tool_patterns"].items(), key=lambda x: -x[1]):
output.append(f" [{count}x] {seq}")
output.append("")
promote_count = sum(1 for p in results["approach_patterns"] if p["recommendation"] == "PROMOTE")
if promote_count:
output.append(f"ACTION: {promote_count} patterns ready for promotion review")
else:
output.append("ACTION: No patterns ready for promotion yet -- continue monitoring")
return "\n".join(output)
def main():
parser = argparse.ArgumentParser(
description="Extract reusable patterns from agent session logs.",
epilog="Example: python pattern_extractor.py --input sessions.jsonl --min-occurrences 3",
)
parser.add_argument("--input", required=True, help="Path to JSONL session log file")
parser.add_argument("--min-occurrences", type=int, default=2, help="Minimum pattern frequency (default: 2)")
parser.add_argument("--days", type=int, default=None, help="Only analyze sessions within N days")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
args = parser.parse_args()
if not Path(args.input).exists():
print(f"Error: Input file '{args.input}' not found.", file=sys.stderr)
sys.exit(1)
entries = load_sessions(args.input, max_days=args.days)
if not entries:
print("No session entries found.", file=sys.stderr)
sys.exit(1)
results = extract_patterns(entries, args.min_occurrences)
if args.json_output:
print(json.dumps(results, indent=2, default=str))
else:
print(format_human(results, len(entries)))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Regression Detector - Compare before/after performance metrics to detect regressions.
Compares two sets of performance data (baseline vs current) to identify
regressions caused by rule changes, memory updates, or other modifications
to the self-improving agent's knowledge base.
Input format (JSON file with baseline and current periods):
{
"baseline": {
"period": "2026-02-01 to 2026-02-15",
"sessions": [
{
"session_id": "s1",
"task_type": "code-review",
"outcome": "SUCCESS",
"corrections": 0,
"turns": 4,
"tool_calls": 12,
"tool_errors": 0,
"context_items_retrieved": 5,
"context_items_used": 4
}
]
},
"current": {
"period": "2026-03-01 to 2026-03-15",
"sessions": [ ... ]
},
"changes": [
{"type": "rule_added", "description": "Added style guide enforcement rule"},
{"type": "rule_promoted", "description": "Promoted pnpm preference to CLAUDE.md"}
]
}
Usage:
python regression_detector.py compare --input metrics.json
python regression_detector.py diagnose --input metrics.json --metric first_attempt_rate
python regression_detector.py report --input metrics.json
"""
import argparse
import json
import os
import sys
from collections import defaultdict
from datetime import datetime
# Thresholds from the SKILL.md regression detection framework
REGRESSION_THRESHOLDS = {
"first_attempt_rate": {"direction": "higher_is_better", "warning": -0.05, "critical": -0.10},
"success_rate": {"direction": "higher_is_better", "warning": -0.05, "critical": -0.10},
"avg_corrections": {"direction": "lower_is_better", "warning": 0.5, "critical": 1.0},
"tool_error_rate": {"direction": "lower_is_better", "warning": 0.02, "critical": 0.05},
"avg_turns": {"direction": "lower_is_better", "warning": 2.0, "critical": 4.0},
"context_relevance": {"direction": "higher_is_better", "warning": -0.10, "critical": -0.20},
}
def load_data(path: str) -> dict:
"""Load comparison data from JSON file."""
with open(path, "r") as f:
return json.load(f)
def compute_metrics(sessions: list) -> dict:
"""Compute aggregate metrics from a list of sessions."""
if not sessions:
return {}
total = len(sessions)
successes = sum(1 for s in sessions if s.get("outcome") == "SUCCESS")
first_attempt = sum(
1 for s in sessions
if s.get("outcome") == "SUCCESS" and s.get("corrections", 0) == 0
)
corrections = [s.get("corrections", 0) for s in sessions]
turns = [s.get("turns", 0) for s in sessions]
tool_calls = sum(s.get("tool_calls", 0) for s in sessions)
tool_errors = sum(s.get("tool_errors", 0) for s in sessions)
ctx_retrieved = sum(s.get("context_items_retrieved", 0) for s in sessions)
ctx_used = sum(s.get("context_items_used", 0) for s in sessions)
return {
"total_sessions": total,
"success_rate": round(successes / total, 4) if total else 0,
"first_attempt_rate": round(first_attempt / total, 4) if total else 0,
"avg_corrections": round(sum(corrections) / total, 3) if total else 0,
"avg_turns": round(sum(turns) / total, 2) if total else 0,
"tool_error_rate": round(tool_errors / tool_calls, 4) if tool_calls else 0,
"context_relevance": round(ctx_used / ctx_retrieved, 4) if ctx_retrieved else 0,
"outcome_distribution": {
outcome: sum(1 for s in sessions if s.get("outcome") == outcome)
for outcome in ["SUCCESS", "PARTIAL", "FAILURE", "REJECTION", "TIMEOUT", "ERROR"]
if any(s.get("outcome") == outcome for s in sessions)
},
}
def compute_per_task_metrics(sessions: list) -> dict:
"""Compute metrics broken down by task type."""
by_type = defaultdict(list)
for s in sessions:
by_type[s.get("task_type", "unknown")].append(s)
return {tt: compute_metrics(ss) for tt, ss in by_type.items()}
def classify_delta(metric_name: str, delta: float) -> str:
"""Classify a metric delta as ok, warning, or critical."""
threshold = REGRESSION_THRESHOLDS.get(metric_name)
if not threshold:
return "unknown"
direction = threshold["direction"]
if direction == "higher_is_better":
effective_delta = delta # negative = regression
else:
effective_delta = -delta # positive = regression for lower-is-better
if effective_delta <= threshold["critical"]:
return "critical"
elif effective_delta <= threshold["warning"]:
return "warning"
return "ok"
def cmd_compare(args, data: dict) -> dict:
"""Compare baseline vs current metrics and flag regressions."""
baseline_metrics = compute_metrics(data["baseline"]["sessions"])
current_metrics = compute_metrics(data["current"]["sessions"])
comparisons = []
regressions = []
for metric in REGRESSION_THRESHOLDS:
b_val = baseline_metrics.get(metric, 0)
c_val = current_metrics.get(metric, 0)
delta = c_val - b_val
severity = classify_delta(metric, delta)
entry = {
"metric": metric,
"baseline": b_val,
"current": c_val,
"delta": round(delta, 4),
"severity": severity,
}
comparisons.append(entry)
if severity in ("warning", "critical"):
regressions.append(entry)
# Per-task-type comparison
baseline_by_task = compute_per_task_metrics(data["baseline"]["sessions"])
current_by_task = compute_per_task_metrics(data["current"]["sessions"])
all_tasks = set(list(baseline_by_task.keys()) + list(current_by_task.keys()))
task_regressions = []
for tt in sorted(all_tasks):
b = baseline_by_task.get(tt, {})
c = current_by_task.get(tt, {})
b_rate = b.get("success_rate", 0)
c_rate = c.get("success_rate", 0)
delta = c_rate - b_rate
if delta < -0.1:
task_regressions.append({
"task_type": tt,
"baseline_success": b_rate,
"current_success": c_rate,
"delta": round(delta, 4),
})
return {
"action": "compare",
"baseline_period": data["baseline"].get("period", "unknown"),
"current_period": data["current"].get("period", "unknown"),
"baseline_sessions": baseline_metrics["total_sessions"],
"current_sessions": current_metrics["total_sessions"],
"comparisons": comparisons,
"regressions": regressions,
"task_regressions": task_regressions,
"regression_detected": len(regressions) > 0,
"changes_applied": data.get("changes", []),
}
def cmd_diagnose(args, data: dict) -> dict:
"""Diagnose a specific metric regression by analyzing contributing factors."""
metric = args.metric
if metric not in REGRESSION_THRESHOLDS:
return {"action": "diagnose", "error": f"Unknown metric: {metric}. Valid: {list(REGRESSION_THRESHOLDS.keys())}"}
baseline_sessions = data["baseline"]["sessions"]
current_sessions = data["current"]["sessions"]
baseline_metrics = compute_metrics(baseline_sessions)
current_metrics = compute_metrics(current_sessions)
b_val = baseline_metrics.get(metric, 0)
c_val = current_metrics.get(metric, 0)
delta = c_val - b_val
severity = classify_delta(metric, delta)
# Identify which task types contributed most to the regression
baseline_by_task = compute_per_task_metrics(baseline_sessions)
current_by_task = compute_per_task_metrics(current_sessions)
contributing_tasks = []
for tt in set(list(baseline_by_task.keys()) + list(current_by_task.keys())):
b = baseline_by_task.get(tt, {}).get(metric, 0)
c = current_by_task.get(tt, {}).get(metric, 0)
task_delta = c - b
task_severity = classify_delta(metric, task_delta)
if task_severity != "ok":
contributing_tasks.append({
"task_type": tt,
"baseline": b,
"current": c,
"delta": round(task_delta, 4),
"severity": task_severity,
})
contributing_tasks.sort(key=lambda x: abs(x["delta"]), reverse=True)
# Generate recommendations
recommendations = []
changes = data.get("changes", [])
if severity == "critical":
recommendations.append("ROLLBACK: Consider reverting recent rule changes immediately")
if contributing_tasks:
top = contributing_tasks[0]
recommendations.append(f"INVESTIGATE: '{top['task_type']}' tasks show largest regression ({top['delta']:+.2%})")
if changes:
recommendations.append(f"REVIEW: {len(changes)} changes applied between periods -- test each in isolation")
recommendations.append(f"MONITOR: Track '{metric}' for next 3 sessions after any fix")
return {
"action": "diagnose",
"metric": metric,
"baseline_value": b_val,
"current_value": c_val,
"delta": round(delta, 4),
"severity": severity,
"contributing_tasks": contributing_tasks,
"changes_between_periods": changes,
"recommendations": recommendations,
}
def cmd_report(args, data: dict) -> dict:
"""Generate a full regression report combining comparison and diagnosis."""
compare_result = cmd_compare(args, data)
diagnosed = []
for reg in compare_result["regressions"]:
diag_args = argparse.Namespace(metric=reg["metric"], input=args.input)
diagnosis = cmd_diagnose(diag_args, data)
diagnosed.append(diagnosis)
overall_status = "PASS"
if any(r["severity"] == "critical" for r in compare_result["regressions"]):
overall_status = "CRITICAL"
elif compare_result["regressions"]:
overall_status = "WARNING"
return {
"action": "report",
"overall_status": overall_status,
"baseline_period": compare_result["baseline_period"],
"current_period": compare_result["current_period"],
"summary": {
"total_metrics_checked": len(compare_result["comparisons"]),
"regressions_found": len(compare_result["regressions"]),
"critical_regressions": sum(1 for r in compare_result["regressions"] if r["severity"] == "critical"),
"warning_regressions": sum(1 for r in compare_result["regressions"] if r["severity"] == "warning"),
"task_type_regressions": len(compare_result["task_regressions"]),
},
"comparisons": compare_result["comparisons"],
"diagnoses": diagnosed,
"changes_applied": compare_result["changes_applied"],
"generated_at": datetime.now().isoformat(),
}
def format_human(result: dict) -> str:
"""Format result for human-readable output."""
action = result.get("action", "unknown")
lines = []
if "error" in result:
return f"Error: {result['error']}"
if action == "compare":
status = "REGRESSION DETECTED" if result["regression_detected"] else "NO REGRESSION"
lines.append(f"Regression Comparison: {status}")
lines.append(f" Baseline: {result['baseline_period']} ({result['baseline_sessions']} sessions)")
lines.append(f" Current: {result['current_period']} ({result['current_sessions']} sessions)")
lines.append("")
lines.append(f"{'Metric':<25} {'Baseline':>10} {'Current':>10} {'Delta':>10} {'Status':>10}")
lines.append("-" * 70)
for c in result["comparisons"]:
indicator = {"ok": " OK", "warning": " WARN", "critical": " CRIT"}.get(c["severity"], " ?")
lines.append(
f"{c['metric']:<25} {c['baseline']:>10.4f} {c['current']:>10.4f} "
f"{c['delta']:>+10.4f} {indicator:>10}"
)
if result["task_regressions"]:
lines.append("")
lines.append("Task-Level Regressions:")
for tr in result["task_regressions"]:
lines.append(f" {tr['task_type']}: {tr['baseline_success']:.0%} -> {tr['current_success']:.0%} ({tr['delta']:+.1%})")
if result["changes_applied"]:
lines.append("")
lines.append("Changes Applied Between Periods:")
for ch in result["changes_applied"]:
lines.append(f" [{ch.get('type', '?')}] {ch.get('description', '')}")
elif action == "diagnose":
lines.append(f"Diagnosis: {result['metric']}")
lines.append(f" Severity: {result['severity'].upper()}")
lines.append(f" Baseline: {result['baseline_value']:.4f}")
lines.append(f" Current: {result['current_value']:.4f}")
lines.append(f" Delta: {result['delta']:+.4f}")
if result["contributing_tasks"]:
lines.append("")
lines.append("Contributing Task Types:")
for ct in result["contributing_tasks"]:
lines.append(f" {ct['task_type']}: {ct['baseline']:.4f} -> {ct['current']:.4f} ({ct['delta']:+.4f}) [{ct['severity']}]")
if result["changes_between_periods"]:
lines.append("")
lines.append("Changes to Review:")
for ch in result["changes_between_periods"]:
lines.append(f" [{ch.get('type', '?')}] {ch.get('description', '')}")
lines.append("")
lines.append("Recommendations:")
for rec in result["recommendations"]:
lines.append(f" -> {rec}")
elif action == "report":
lines.append(f"{'=' * 60}")
lines.append(f" REGRESSION REPORT: {result['overall_status']}")
lines.append(f"{'=' * 60}")
lines.append(f" Baseline: {result['baseline_period']}")
lines.append(f" Current: {result['current_period']}")
lines.append(f" Generated: {result['generated_at'][:19]}")
lines.append("")
s = result["summary"]
lines.append(f" Metrics checked: {s['total_metrics_checked']}")
lines.append(f" Regressions found: {s['regressions_found']}")
lines.append(f" Critical: {s['critical_regressions']}")
lines.append(f" Warning: {s['warning_regressions']}")
lines.append(f" Task-type regressions: {s['task_type_regressions']}")
lines.append("")
lines.append("Metric Details:")
lines.append(f" {'Metric':<25} {'Baseline':>10} {'Current':>10} {'Delta':>10} {'Status':>8}")
lines.append(" " + "-" * 66)
for c in result["comparisons"]:
tag = {"ok": "OK", "warning": "WARN", "critical": "CRIT"}.get(c["severity"], "?")
lines.append(
f" {c['metric']:<25} {c['baseline']:>10.4f} {c['current']:>10.4f} "
f"{c['delta']:>+10.4f} {tag:>8}"
)
for diag in result.get("diagnoses", []):
lines.append("")
lines.append(f" --- Diagnosis: {diag['metric']} [{diag['severity'].upper()}] ---")
if diag.get("contributing_tasks"):
for ct in diag["contributing_tasks"]:
lines.append(f" {ct['task_type']}: {ct['delta']:+.4f} [{ct['severity']}]")
for rec in diag.get("recommendations", []):
lines.append(f" -> {rec}")
if result["changes_applied"]:
lines.append("")
lines.append(" Changes Applied:")
for ch in result["changes_applied"]:
lines.append(f" [{ch.get('type', '?')}] {ch.get('description', '')}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Regression Detector - Compare before/after performance metrics",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--json", action="store_true", dest="json_output", help="Output in JSON format")
sub = parser.add_subparsers(dest="command", help="Available commands")
p_compare = sub.add_parser("compare", help="Compare baseline vs current metrics")
p_compare.add_argument("--input", required=True, help="Path to metrics JSON file")
p_diagnose = sub.add_parser("diagnose", help="Diagnose a specific metric regression")
p_diagnose.add_argument("--input", required=True, help="Path to metrics JSON file")
p_diagnose.add_argument("--metric", required=True, choices=list(REGRESSION_THRESHOLDS.keys()),
help="Metric to diagnose")
p_report = sub.add_parser("report", help="Generate full regression report")
p_report.add_argument("--input", required=True, help="Path to metrics JSON file")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
if not os.path.exists(args.input):
print(f"Error: Input file '{args.input}' not found", file=sys.stderr)
sys.exit(1)
data = load_data(args.input)
commands = {
"compare": cmd_compare,
"diagnose": cmd_diagnose,
"report": cmd_report,
}
result = commands[args.command](args, data)
if args.json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(format_human(result))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Rule Manager - Manage a learned rules knowledge base.
Add, list, search, prune, and promote/demote rules with confidence scores.
Rules are stored as JSON in a local knowledge base file, following the
Self-Improving Agent confidence scoring model.
Usage:
python rule_manager.py add --rule "Use pnpm not npm" --source observed --category tool-preference
python rule_manager.py list --sort confidence
python rule_manager.py search --query "test"
python rule_manager.py prune --max-age 90 --min-confidence 0.3
python rule_manager.py promote --id <rule-id>
python rule_manager.py demote --id <rule-id> --reason "contradicted by new pattern"
"""
import argparse
import json
import os
import sys
import uuid
from datetime import datetime, timedelta
from pathlib import Path
DEFAULT_DB = "rules_kb.json"
VALID_SOURCES = ["user-stated", "observed", "inferred", "guessed"]
SOURCE_SCORES = {"user-stated": 1.0, "observed": 0.8, "inferred": 0.6, "guessed": 0.3}
VALID_STATUSES = ["candidate", "promoted", "stale", "retired"]
VALID_CATEGORIES = [
"coding-convention", "project-architecture", "tool-preference",
"debugging-pattern", "style-guide", "api-usage", "git-workflow",
"testing", "performance", "security", "other",
]
def load_db(path: str) -> dict:
"""Load the rules database from disk."""
if os.path.exists(path):
with open(path, "r") as f:
return json.load(f)
return {"rules": [], "metadata": {"created": datetime.now().isoformat(), "version": "1.0.0"}}
def save_db(db: dict, path: str) -> None:
"""Persist the rules database to disk."""
db["metadata"]["updated"] = datetime.now().isoformat()
with open(path, "w") as f:
json.dump(db, f, indent=2)
def compute_confidence(rule: dict) -> float:
"""Compute effective confidence using base * recency * consistency factors."""
base = SOURCE_SCORES.get(rule.get("source", "guessed"), 0.3)
updated = datetime.fromisoformat(rule["updated"])
age_days = (datetime.now() - updated).days
if age_days <= 7:
recency = 1.0
elif age_days <= 30:
recency = 0.9
elif age_days <= 90:
recency = 0.7
else:
recency = 0.5
consistency = rule.get("consistency_factor", 1.0)
return round(base * recency * consistency, 3)
def cmd_add(args, db: dict) -> dict:
"""Add a new rule to the knowledge base."""
rule_id = str(uuid.uuid4())[:8]
now = datetime.now().isoformat()
rule = {
"id": rule_id,
"rule": args.rule,
"source": args.source,
"category": args.category,
"status": "candidate",
"recurrence": 1,
"consistency_factor": 1.0,
"created": now,
"updated": now,
"notes": args.notes or "",
}
rule["confidence"] = compute_confidence(rule)
db["rules"].append(rule)
return {"action": "added", "rule": rule}
def cmd_list(args, db: dict) -> dict:
"""List rules with optional filtering and sorting."""
rules = db["rules"]
if args.status:
rules = [r for r in rules if r["status"] == args.status]
if args.category:
rules = [r for r in rules if r["category"] == args.category]
# Recompute confidence for all listed rules
for r in rules:
r["confidence"] = compute_confidence(r)
sort_key = args.sort if args.sort else "confidence"
reverse = sort_key in ("confidence", "recurrence")
rules = sorted(rules, key=lambda r: r.get(sort_key, ""), reverse=reverse)
if args.limit:
rules = rules[: args.limit]
return {"action": "list", "count": len(rules), "rules": rules}
def cmd_search(args, db: dict) -> dict:
"""Search rules by text query across rule text, notes, and category."""
query = args.query.lower()
matches = []
for r in db["rules"]:
searchable = f"{r['rule']} {r.get('notes', '')} {r['category']}".lower()
if query in searchable:
r["confidence"] = compute_confidence(r)
matches.append(r)
matches.sort(key=lambda r: r["confidence"], reverse=True)
return {"action": "search", "query": args.query, "count": len(matches), "rules": matches}
def cmd_prune(args, db: dict) -> dict:
"""Remove rules that are stale or below confidence threshold."""
now = datetime.now()
pruned = []
kept = []
for r in db["rules"]:
r["confidence"] = compute_confidence(r)
age_days = (now - datetime.fromisoformat(r["updated"])).days
should_prune = False
reasons = []
if args.max_age and age_days > args.max_age:
should_prune = True
reasons.append(f"age={age_days}d > {args.max_age}d")
if args.min_confidence and r["confidence"] < args.min_confidence:
should_prune = True
reasons.append(f"confidence={r['confidence']} < {args.min_confidence}")
if r["status"] == "retired":
should_prune = True
reasons.append("status=retired")
if should_prune and not args.dry_run:
r["prune_reasons"] = reasons
pruned.append(r)
elif should_prune:
r["prune_reasons"] = reasons
pruned.append(r)
kept.append(r) # dry run keeps them
else:
kept.append(r)
if not args.dry_run:
db["rules"] = kept
return {
"action": "prune",
"dry_run": args.dry_run,
"pruned_count": len(pruned),
"remaining_count": len(db["rules"]),
"pruned": pruned,
}
def cmd_promote(args, db: dict) -> dict:
"""Promote a rule from candidate to promoted status."""
for r in db["rules"]:
if r["id"] == args.id:
old_status = r["status"]
if old_status == "promoted":
return {"action": "promote", "error": f"Rule {args.id} is already promoted"}
r["status"] = "promoted"
r["updated"] = datetime.now().isoformat()
r["recurrence"] = max(r["recurrence"], 3)
r["confidence"] = compute_confidence(r)
return {
"action": "promote",
"id": args.id,
"old_status": old_status,
"new_status": "promoted",
"rule": r,
}
return {"action": "promote", "error": f"Rule {args.id} not found"}
def cmd_demote(args, db: dict) -> dict:
"""Demote a rule by reducing consistency and optionally retiring it."""
for r in db["rules"]:
if r["id"] == args.id:
old_status = r["status"]
r["consistency_factor"] = max(0.0, r.get("consistency_factor", 1.0) - 0.3)
if r["consistency_factor"] <= 0.0:
r["status"] = "retired"
elif args.retire:
r["status"] = "retired"
else:
r["status"] = "stale"
r["updated"] = datetime.now().isoformat()
r["notes"] = f"{r.get('notes', '')} [Demoted: {args.reason}]".strip()
r["confidence"] = compute_confidence(r)
return {
"action": "demote",
"id": args.id,
"old_status": old_status,
"new_status": r["status"],
"new_confidence": r["confidence"],
"reason": args.reason,
"rule": r,
}
return {"action": "demote", "error": f"Rule {args.id} not found"}
def format_human(result: dict) -> str:
"""Format result for human-readable output."""
action = result.get("action", "unknown")
lines = []
if "error" in result:
return f"Error: {result['error']}"
if action == "added":
r = result["rule"]
lines.append(f"Added rule [{r['id']}]: {r['rule']}")
lines.append(f" Source: {r['source']} Category: {r['category']} Confidence: {r['confidence']}")
elif action == "list":
lines.append(f"Rules ({result['count']} total):")
lines.append(f"{'ID':<10} {'Status':<12} {'Conf':<7} {'Rec':<5} {'Category':<22} Rule")
lines.append("-" * 100)
for r in result["rules"]:
lines.append(
f"{r['id']:<10} {r['status']:<12} {r['confidence']:<7} "
f"{r['recurrence']:<5} {r['category']:<22} {r['rule'][:50]}"
)
elif action == "search":
lines.append(f"Search results for '{result['query']}' ({result['count']} matches):")
for r in result["rules"]:
lines.append(f" [{r['id']}] (conf={r['confidence']}) {r['rule']}")
elif action == "prune":
mode = " (DRY RUN)" if result["dry_run"] else ""
lines.append(f"Prune{mode}: {result['pruned_count']} removed, {result['remaining_count']} remaining")
for r in result["pruned"]:
reasons = ", ".join(r.get("prune_reasons", []))
lines.append(f" [{r['id']}] {r['rule'][:50]} -- {reasons}")
elif action in ("promote", "demote"):
r = result.get("rule", {})
lines.append(f"{action.title()}: [{result['id']}] {r.get('rule', '')}")
lines.append(f" {result.get('old_status', '')} -> {result.get('new_status', '')}")
if "reason" in result:
lines.append(f" Reason: {result['reason']}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Rule Manager - Manage a learned rules knowledge base",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--db", default=DEFAULT_DB, help="Path to rules database file (default: rules_kb.json)")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output in JSON format")
sub = parser.add_subparsers(dest="command", help="Available commands")
# add
p_add = sub.add_parser("add", help="Add a new rule")
p_add.add_argument("--rule", required=True, help="The rule text")
p_add.add_argument("--source", choices=VALID_SOURCES, default="observed", help="How the rule was learned")
p_add.add_argument("--category", choices=VALID_CATEGORIES, default="other", help="Rule category")
p_add.add_argument("--notes", help="Additional context or notes")
# list
p_list = sub.add_parser("list", help="List rules")
p_list.add_argument("--sort", choices=["confidence", "recurrence", "created", "updated", "category"], default="confidence")
p_list.add_argument("--status", choices=VALID_STATUSES, help="Filter by status")
p_list.add_argument("--category", choices=VALID_CATEGORIES, help="Filter by category")
p_list.add_argument("--limit", type=int, help="Max number of rules to show")
# search
p_search = sub.add_parser("search", help="Search rules by text")
p_search.add_argument("--query", required=True, help="Search query")
# prune
p_prune = sub.add_parser("prune", help="Remove stale or low-confidence rules")
p_prune.add_argument("--max-age", type=int, help="Max age in days before pruning")
p_prune.add_argument("--min-confidence", type=float, help="Min confidence threshold")
p_prune.add_argument("--dry-run", action="store_true", help="Show what would be pruned without removing")
# promote
p_promote = sub.add_parser("promote", help="Promote a rule to enforced status")
p_promote.add_argument("--id", required=True, help="Rule ID to promote")
# demote
p_demote = sub.add_parser("demote", help="Demote a rule (reduce confidence)")
p_demote.add_argument("--id", required=True, help="Rule ID to demote")
p_demote.add_argument("--reason", required=True, help="Why the rule is being demoted")
p_demote.add_argument("--retire", action="store_true", help="Immediately retire the rule")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
db = load_db(args.db)
commands = {
"add": cmd_add,
"list": cmd_list,
"search": cmd_search,
"prune": cmd_prune,
"promote": cmd_promote,
"demote": cmd_demote,
}
result = commands[args.command](args, db)
save_db(db, args.db)
if args.json_output:
print(json.dumps(result, indent=2))
else:
print(format_human(result))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Analyze pattern frequency and promote memory entries to permanent rules.
Validates promotion criteria for memory entries and generates rule text
suitable for CLAUDE.md or .claude/rules/ files. Supports dry-run mode
to preview changes before applying.
Usage:
python rule_promoter.py --memory ./MEMORY.md --entry-title "Use pnpm" --target claude-md --dry-run
python rule_promoter.py --memory ./MEMORY.md --entry-title "Use pnpm" --target rules-dir --apply
python rule_promoter.py --memory ./MEMORY.md --list-candidates
python rule_promoter.py --memory ./MEMORY.md --list-candidates --json
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
PROMOTION_CRITERIA = {
"min_recurrence": 3,
"required_confidence": ["high", "medium"],
"forbidden_actions": ["EXTRACT"],
}
RULE_TEMPLATES = {
"coding-convention": "**{rule}** -- {reason}",
"tool-preference": "**{rule}** -- {reason}",
"project-architecture": "- {rule} ({reason})",
"debugging-pattern": "When debugging: {rule} ({reason})",
"style-guide": "- {rule}",
"default": "- {rule} -- {reason}",
}
def load_memory(path):
"""Load memory file and parse entries."""
if not os.path.exists(path):
print(f"Error: Memory file '{path}' not found.", file=sys.stderr)
sys.exit(1)
content = Path(path).read_text(encoding="utf-8")
lines = content.split("\n")
entries = []
current = None
for i, line in enumerate(lines, 1):
if line.startswith("## "):
if current:
current["end_line"] = i - 1
current["content"] = "\n".join(current["raw_lines"])
entries.append(current)
current = {
"title": line.lstrip("# ").strip(),
"start_line": i,
"end_line": None,
"raw_lines": [],
"metadata": {},
}
elif current:
current["raw_lines"].append(line)
kv = re.match(r"\*\*(\w[\w\s]*)\*\*:\s*(.+)", line)
if kv:
current["metadata"][kv.group(1).strip().lower()] = kv.group(2).strip()
if current:
current["end_line"] = len(lines)
current["content"] = "\n".join(current["raw_lines"])
entries.append(current)
return content, lines, entries
def get_recurrence(entry):
"""Extract recurrence count from entry metadata."""
rec_str = entry["metadata"].get("recurrence", "")
match = re.search(r"(\d+)", rec_str)
return int(match.group(1)) if match else 1
def validate_promotion(entry):
"""Check if an entry meets all promotion criteria."""
reasons = []
passes = True
recurrence = get_recurrence(entry)
if recurrence < PROMOTION_CRITERIA["min_recurrence"]:
passes = False
reasons.append(f"Recurrence {recurrence} < minimum {PROMOTION_CRITERIA['min_recurrence']}")
confidence = entry["metadata"].get("confidence", "").lower()
if confidence and confidence not in PROMOTION_CRITERIA["required_confidence"]:
passes = False
reasons.append(f"Confidence '{confidence}' not in required: {PROMOTION_CRITERIA['required_confidence']}")
action = entry["metadata"].get("action", "").upper()
if action in PROMOTION_CRITERIA["forbidden_actions"]:
passes = False
reasons.append(f"Action '{action}' is marked for extraction, not promotion")
# Check clarity: title should be concise
if len(entry["title"]) > 100:
reasons.append("Title is over 100 characters -- consider making it more concise")
return {
"valid": passes,
"reasons": reasons,
"recurrence": recurrence,
"confidence": confidence,
}
def generate_rule_text(entry, category="default"):
"""Generate formatted rule text from a memory entry."""
title = entry["title"]
# Extract the core rule from the title
# Remove "Learning: " prefix if present
rule = re.sub(r"^Learning:\s*", "", title, flags=re.IGNORECASE).strip()
reason = entry["metadata"].get("root cause", "")
if not reason:
reason = entry["metadata"].get("correct approach", "")
if not reason:
# Try to extract from content
for line in entry["raw_lines"]:
if "because" in line.lower() or "reason" in line.lower():
reason = line.strip().lstrip("- ").strip()
break
template = RULE_TEMPLATES.get(category, RULE_TEMPLATES["default"])
return template.format(rule=rule, reason=reason or "proven pattern")
def find_candidates(entries):
"""Find all entries that meet promotion criteria."""
candidates = []
for entry in entries:
validation = validate_promotion(entry)
if validation["valid"]:
candidates.append({
"title": entry["title"],
"start_line": entry["start_line"],
"recurrence": validation["recurrence"],
"confidence": validation["confidence"],
"suggested_rule": generate_rule_text(entry),
})
elif validation["recurrence"] >= 2:
# Near-ready candidates
candidates.append({
"title": entry["title"],
"start_line": entry["start_line"],
"recurrence": validation["recurrence"],
"confidence": validation["confidence"],
"suggested_rule": generate_rule_text(entry),
"blockers": validation["reasons"],
})
candidates.sort(key=lambda c: -c["recurrence"])
return candidates
def remove_entry_from_memory(content, lines, entry):
"""Remove a promoted entry from memory content."""
start = entry["start_line"] - 1 # 0-indexed
end = entry["end_line"] # already past-the-end
new_lines = lines[:start] + lines[end:]
# Clean up double blank lines
cleaned = []
prev_blank = False
for line in new_lines:
is_blank = line.strip() == ""
if is_blank and prev_blank:
continue
cleaned.append(line)
prev_blank = is_blank
return "\n".join(cleaned)
def apply_promotion(memory_path, entry, target, target_path):
"""Apply the promotion: add rule to target, remove from memory."""
rule_text = generate_rule_text(entry)
now = datetime.now().strftime("%Y-%m-%d")
if target == "claude-md":
# Append to CLAUDE.md
if os.path.exists(target_path):
existing = Path(target_path).read_text(encoding="utf-8")
else:
existing = ""
addition = f"\n{rule_text} <!-- promoted {now} -->\n"
Path(target_path).write_text(existing + addition, encoding="utf-8")
elif target == "rules-dir":
# Create or append to rules file
rules_dir = Path(target_path)
rules_dir.mkdir(parents=True, exist_ok=True)
rule_file = rules_dir / "promoted-rules.md"
existing = rule_file.read_text(encoding="utf-8") if rule_file.exists() else "# Promoted Rules\n\n"
existing += f"\n{rule_text} <!-- promoted {now} -->\n"
rule_file.write_text(existing, encoding="utf-8")
# Remove from memory
content, lines, entries = load_memory(memory_path)
new_content = remove_entry_from_memory(content, lines, entry)
# Add promotion note
note = f"\n<!-- Promoted to {target} on {now}: {entry['title'][:50]} -->\n"
Path(memory_path).write_text(new_content + note, encoding="utf-8")
return {
"promoted": True,
"rule_text": rule_text,
"target": target,
"target_path": target_path,
"removed_from_memory": True,
}
def format_human(result):
"""Format result for human output."""
lines = []
if "candidates" in result:
lines.append("=" * 60)
lines.append("PROMOTION CANDIDATES")
lines.append("=" * 60)
ready = [c for c in result["candidates"] if "blockers" not in c]
near_ready = [c for c in result["candidates"] if "blockers" in c]
if ready:
lines.append(f"\nREADY FOR PROMOTION ({len(ready)})")
lines.append("-" * 60)
for c in ready:
lines.append(f" >> Line {c['start_line']}: {c['title'][:50]}")
lines.append(f" Recurrence: {c['recurrence']} | Confidence: {c['confidence']}")
lines.append(f" Rule: {c['suggested_rule'][:60]}")
lines.append("")
if near_ready:
lines.append(f"\nNEAR-READY ({len(near_ready)})")
lines.append("-" * 60)
for c in near_ready:
lines.append(f" .. Line {c['start_line']}: {c['title'][:50]}")
lines.append(f" Recurrence: {c['recurrence']} | Blockers: {'; '.join(c['blockers'][:2])}")
lines.append("")
elif "validation" in result:
v = result["validation"]
status = "READY" if v["valid"] else "NOT READY"
lines.append(f"Promotion Validation: {status}")
lines.append(f" Entry: {result['entry_title']}")
lines.append(f" Recurrence: {v['recurrence']}")
if v["reasons"]:
lines.append(f" Issues: {'; '.join(v['reasons'])}")
if "rule_text" in result:
lines.append(f" Generated rule: {result['rule_text']}")
elif "promoted" in result:
lines.append(f"Promotion Applied")
lines.append(f" Rule: {result['rule_text']}")
lines.append(f" Target: {result['target']} ({result['target_path']})")
lines.append(f" Removed from memory: {result['removed_from_memory']}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze pattern frequency and promote memory entries to permanent rules.",
)
parser.add_argument("--memory", required=True, help="Path to MEMORY.md")
parser.add_argument("--list-candidates", action="store_true", help="List all promotion candidates")
parser.add_argument("--entry-title", help="Title of specific entry to promote")
parser.add_argument("--target", choices=["claude-md", "rules-dir"], help="Promotion target")
parser.add_argument("--target-path", help="Path to target file/directory")
parser.add_argument("--dry-run", action="store_true", help="Preview without applying")
parser.add_argument("--apply", action="store_true", help="Apply the promotion")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
args = parser.parse_args()
content, file_lines, entries = load_memory(args.memory)
if args.list_candidates:
candidates = find_candidates(entries)
result = {"candidates": candidates, "total_entries": len(entries)}
elif args.entry_title:
# Find the specific entry
target_entry = None
for e in entries:
if args.entry_title.lower() in e["title"].lower():
target_entry = e
break
if not target_entry:
print(f"Error: Entry matching '{args.entry_title}' not found.", file=sys.stderr)
sys.exit(1)
validation = validate_promotion(target_entry)
rule_text = generate_rule_text(target_entry)
if args.apply and validation["valid"] and args.target and args.target_path:
result = apply_promotion(args.memory, target_entry, args.target, args.target_path)
else:
result = {
"entry_title": target_entry["title"],
"validation": validation,
"rule_text": rule_text,
"dry_run": True,
}
else:
parser.print_help()
sys.exit(1)
if args.json_output:
print(json.dumps(result, indent=2))
else:
print(format_human(result))
if __name__ == "__main__":
main()
Sub-Skill: Extract
Parent: self-improving-agent Trigger: "extract patterns", "find reusable patterns", "what did we learn"
Purpose
Extract reusable patterns from completed work sessions. Analyzes session history to identify approaches that succeeded consistently and packages them as candidate rules or skill components.
Workflow
Step 1: Gather Session Data
Collect recent session outcomes:
- Tasks completed and their outcomes (success, partial, failure)
- Corrections made by the user
- Tool usage patterns
- Error resolutions applied
Step 2: Identify Patterns
Run pattern_extractor.py on session logs:
python scripts/pattern_extractor.py --input session-log.jsonl --min-occurrences 2Pattern categories:
- Solution patterns: Same approach solved similar problems multiple times
- Error patterns: Same error occurred and was resolved the same way
- Workflow patterns: A sequence of steps was effective repeatedly
- Anti-patterns: Approaches that consistently failed
Step 3: Score Patterns
Each pattern gets scored on:
- Frequency: How often it appeared (2-3 = low, 4-6 = medium, 7+ = high)
- Consistency: Same solution every time vs varied approaches
- Impact: Prevented errors (high) vs minor convenience (low)
- Generalizability: Works across contexts vs project-specific
Step 4: Package Candidates
For each high-scoring pattern, create a candidate entry:
Pattern: [description]
Evidence: [N occurrences across M sessions]
Score: [frequency * consistency * impact]
Recommendation: KEEP | PROMOTE | EXTRACT_TO_SKILLStep 5: Present for Review
Output the ranked pattern list for human review. Patterns are candidates, not automatically promoted -- the promote sub-skill handles graduation.
Inputs
| Input | Required | Description |
|---|---|---|
| Session logs | Yes | JSONL file of session outcomes |
| Min occurrences | No | Minimum pattern frequency (default: 2) |
| Time range | No | Only analyze sessions within N days |
Outputs
- Ranked list of extracted patterns with scores
- Candidate entries ready for promotion review
- Anti-pattern warnings
Sub-Skill: Promote
Parent: self-improving-agent Trigger: "promote pattern to rule", "graduate to CLAUDE.md", "make this permanent"
Purpose
Graduate proven patterns from memory (MEMORY.md) to enforced rules (CLAUDE.md or .claude/rules/). This is the critical step that turns observations into permanent behavior changes.
Workflow
Step 1: Check Promotion Criteria
A pattern is ready for promotion when ALL of these are met:
| Criterion | Threshold | Verification |
|---|---|---|
| Recurrence | 3+ sessions | Check recurrence count in memory |
| Consistency | Same solution every time | No contradicting entries exist |
| Impact | Prevented errors or saved time | At least one error prevented |
| Stability | Underlying system unchanged | Referenced code/tools still exist |
| Clarity | Statable in 1-2 sentences | Can be expressed as a clear rule |
Step 2: Determine Target
| Pattern Type | Promote To | Format |
|---|---|---|
| Coding convention | .claude/rules/<area>.md | Rule with scope path |
| Project architecture | CLAUDE.md | Architecture section entry |
| Tool preference | CLAUDE.md | Development environment section |
| Debugging pattern | .claude/rules/debugging.md | Conditional rule |
| File-scoped rule | .claude/rules/<scope>.md with paths: | Scoped rule |
Step 3: Draft the Rule
Format as a clear, enforceable statement:
- Start with an action verb (Use, Always, Never, Prefer)
- Include the "why" as a brief annotation
- Scope to specific files/directories if applicable
Example:
Always use `type` not `interface` for object shapes.
Reason: Consistent with codebase convention; types are more flexible for unions.Step 4: Apply Promotion
Use rule_promoter.py to validate and apply:
python scripts/rule_promoter.py --memory-entry <id> --target claude-md --dry-run
python scripts/rule_promoter.py --memory-entry <id> --target claude-md --applyStep 5: Clean Up Memory
After promotion:
- Remove the original entry from MEMORY.md
- Add a reference note: "Promoted to CLAUDE.md on [date]"
- Verify MEMORY.md line count is within limits
Inputs
| Input | Required | Description |
|---|---|---|
| Memory entry ID | Yes | The pattern/entry to promote |
| Target | Yes | CLAUDE.md or .claude/rules/<name>.md |
| Dry run | No | Preview without applying (default: true) |
Outputs
- Promotion validation result (pass/fail with reasons)
- Draft rule text
- Applied changes (if not dry run)
- Updated memory with entry removed
Sub-Skill: Remember
Parent: self-improving-agent Trigger: "remember this", "capture learning", "log what happened", "save this error"
Purpose
Capture errors, corrections, and learnings from the current session into the memory system. This is the entry point for the self-improvement loop -- nothing can be improved if it is not first recorded.
Workflow
Step 1: Classify the Event
Determine what type of learning to capture:
| Type | Signal | Example |
|---|---|---|
| Error resolution | A tool error was fixed | "Bash command failed because path had spaces" |
| User correction | User edited agent output | "User changed import path from relative to absolute" |
| Pattern discovery | A reusable approach worked | "Using test.beforeEach eliminated shared state bugs" |
| Anti-pattern | An approach repeatedly fails | "Never use cy.wait() -- always use assertions" |
| Preference | User stated a preference | "Use pnpm, not npm" |
Step 2: Record the Learning
Format the entry using the feedback capture template:
## Learning: [Short description]
**Context:** [What task was being performed]
**What happened:** [Outcome description]
**Root cause:** [Why the outcome occurred]
**Correct approach:** [What should have been done]
**Confidence:** [High/Medium/Low]
**Recurrence:** [First time / Seen N times]
**Action:** [KEEP / PROMOTE / EXTRACT]Step 3: Check for Duplicates
Before adding, search existing memory for related entries:
- If a matching entry exists, increment its recurrence count
- If recurrence crosses the promotion threshold (3+), flag for promotion
Step 4: Store
Add the entry to the appropriate location:
- MEMORY.md for general learnings
memory/<topic>.mdfor topic-specific learnings- Verify MEMORY.md stays under 200 lines
Inputs
| Input | Required | Description |
|---|---|---|
| Description | Yes | What was learned |
| Context | Yes | What task triggered the learning |
| Type | No | Auto-classified from description |
Outputs
- New memory entry with confidence score
- Duplicate detection result (new vs incremented)
- Promotion flag if recurrence threshold met
Sub-Skill: Review Memory Health
Parent: self-improving-agent Trigger: "review memory", "memory health check", "clean up MEMORY.md", "prune stale entries"
Purpose
Audit the memory system for health issues: bloat, stale entries, contradictions, and promotion candidates. This is the maintenance workflow that keeps the self-improvement system effective.
Workflow
Step 1: Load Memory Files
Read all memory sources:
MEMORY.md(primary)memory/<topic>.mdfiles (overflow).claude/rules/files (promoted rules)CLAUDE.mdrules section
Step 2: Classify Each Entry
Use memory_health_checker.py:
python scripts/memory_health_checker.py --memory ./MEMORY.md --rules ./.claude/rules/Classification categories:
| Category | Criteria | Action |
|---|---|---|
| PROMOTE | 3+ recurrences, consistent, impactful | Move to rules |
| CONSOLIDATE | Multiple entries saying the same thing | Merge into one |
| STALE | References deleted files or resolved issues | Delete |
| KEEP | Still relevant, not yet proven enough | Leave in place |
| EXTRACT | Recurring solution worth packaging | Create skill |
| CONTRADICTION | Conflicts with another entry or rule | Resolve |
Step 3: Check Constraints
- MEMORY.md under 200 lines?
- Any topic files over 100 lines?
- Any rules without a "why" annotation?
- Any rules older than 90 days without re-verification?
Step 4: Execute Actions
With user confirmation: 1. Promote ready entries (delegate to promote sub-skill) 2. Merge duplicate entries 3. Delete stale entries 4. Flag contradictions for resolution 5. Move overflow to topic files
Step 5: Report
Output health report:
Memory Health Report
Total entries: 47
PROMOTE: 5 (ready for graduation)
CONSOLIDATE: 8 (duplicates to merge)
STALE: 3 (safe to delete)
KEEP: 29 (healthy)
CONTRADICTION: 2 (need resolution)
Line count: 187/200Inputs
| Input | Required | Description |
|---|---|---|
| Memory path | No | Defaults to ./MEMORY.md |
| Rules path | No | Defaults to ./.claude/rules/ |
| Auto-apply | No | Apply non-destructive actions automatically |
Outputs
- Health classification for every entry
- Constraint violation warnings
- Actionable recommendations (prioritized)
- Post-cleanup statistics
Sub-Skill: Status
Parent: self-improving-agent Trigger: "memory status", "learning progress", "how am I improving", "show improvement metrics"
Purpose
Display the current state of the self-improvement system: memory size, rule counts, learning velocity, and improvement trends. Provides a dashboard view of agent learning progress.
Workflow
Step 1: Gather Metrics
Collect data from all improvement system components:
Memory metrics:
- Total entries in MEMORY.md
- Entries per topic file
- Line counts vs limits
- Oldest and newest entries
Rule metrics:
- Total promoted rules
- Rules by category
- Rules by age (fresh / aging / stale)
- Rules with/without "why" annotations
Learning velocity:
- Entries added in last 7 days
- Entries promoted in last 30 days
- Entries pruned in last 30 days
- Net growth rate
Step 2: Compute Improvement Score
Improvement Score = (
promoted_rules_30d * 0.3 +
(1 - stale_entry_ratio) * 0.2 +
first_attempt_success_delta * 0.3 +
memory_health_score * 0.2
)Where:
promoted_rules_30d: Rules promoted in last 30 days (normalized 0-1)stale_entry_ratio: Fraction of entries classified as stalefirst_attempt_success_delta: Change in first-attempt success ratememory_health_score: 1.0 if under limits, decreasing with violations
Step 3: Determine Maturity Level
Map to the improvement maturity model:
| Level | Name | Indicator |
|---|---|---|
| 0 | Stateless | No MEMORY.md or empty |
| 1 | Recording | Entries exist, no promotions |
| 2 | Curating | Regular reviews, entries classified |
| 3 | Promoting | Active rule promotion pipeline |
| 4 | Extracting | Skills being extracted from patterns |
| 5 | Meta-Learning | Capture strategy adapting based on value |
Step 4: Display Dashboard
Self-Improvement Status
========================
Maturity Level: 3 (Promoting)
Improvement Score: 0.72
Memory: 47 entries (187/200 lines)
Rules: 12 promoted (3 this month)
Velocity: +8 entries, -3 pruned, +3 promoted (30d)
Health: GOOD (no constraint violations)
Trend: IMPROVING (first-attempt success +5% over 30d)Inputs
| Input | Required | Description |
|---|---|---|
| Memory path | No | Defaults to ./MEMORY.md |
| Feedback log | No | Path to feedback data for trend analysis |
Outputs
- Dashboard with current metrics
- Maturity level classification
- Improvement score with breakdown
- Trend direction (improving / stable / degrading)
- Recommended next actions