Initial: Claude config with agents, skills, commands, rules and scripts

This commit is contained in:
2026-02-16 20:21:30 -03:00
commit 8779f3a0a4
153 changed files with 27484 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
---
name: continuous-learning-v2
description: Instinct-based learning system that observes sessions via hooks, creates atomic instincts with confidence scoring, and evolves them into skills/commands/agents.
version: 2.0.0
---
# Continuous Learning v2 - Instinct-Based Architecture
An advanced learning system that turns your Claude Code sessions into reusable knowledge through atomic "instincts" - small learned behaviors with confidence scoring.
## When to Activate
- Setting up automatic learning from Claude Code sessions
- Configuring instinct-based behavior extraction via hooks
- Tuning confidence thresholds for learned behaviors
- Reviewing, exporting, or importing instinct libraries
- Evolving instincts into full skills, commands, or agents
## What's New in v2
| Feature | v1 | v2 |
|---------|----|----|
| Observation | Stop hook (session end) | PreToolUse/PostToolUse (100% reliable) |
| Analysis | Main context | Background agent (Haiku) |
| Granularity | Full skills | Atomic "instincts" |
| Confidence | None | 0.3-0.9 weighted |
| Evolution | Direct to skill | Instincts → cluster → skill/command/agent |
| Sharing | None | Export/import instincts |
## The Instinct Model
An instinct is a small learned behavior:
```yaml
---
id: prefer-functional-style
trigger: "when writing new functions"
confidence: 0.7
domain: "code-style"
source: "session-observation"
---
# Prefer Functional Style
## Action
Use functional patterns over classes when appropriate.
## Evidence
- Observed 5 instances of functional pattern preference
- User corrected class-based approach to functional on 2025-01-15
```
**Properties:**
- **Atomic** — one trigger, one action
- **Confidence-weighted** — 0.3 = tentative, 0.9 = near certain
- **Domain-tagged** — code-style, testing, git, debugging, workflow, etc.
- **Evidence-backed** — tracks what observations created it
## How It Works
```
Session Activity
│ Hooks capture prompts + tool use (100% reliable)
┌─────────────────────────────────────────┐
│ observations.jsonl │
│ (prompts, tool calls, outcomes) │
└─────────────────────────────────────────┘
│ Observer agent reads (background, Haiku)
┌─────────────────────────────────────────┐
│ PATTERN DETECTION │
│ • User corrections → instinct │
│ • Error resolutions → instinct │
│ • Repeated workflows → instinct │
└─────────────────────────────────────────┘
│ Creates/updates
┌─────────────────────────────────────────┐
│ instincts/personal/ │
│ • prefer-functional.md (0.7) │
│ • always-test-first.md (0.9) │
│ • use-zod-validation.md (0.6) │
└─────────────────────────────────────────┘
│ /evolve clusters
┌─────────────────────────────────────────┐
│ evolved/ │
│ • commands/new-feature.md │
│ • skills/testing-workflow.md │
│ • agents/refactor-specialist.md │
└─────────────────────────────────────────┘
```
## Quick Start
### 1. Enable Observation Hooks
Add to your `~/.claude/settings.json`.
**If installed as a plugin** (recommended):
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh pre"
}]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh post"
}]
}]
}
}
```
**If installed manually** to `~/.claude/skills`:
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh pre"
}]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh post"
}]
}]
}
}
```
### 2. Initialize Directory Structure
The Python CLI will create these automatically, but you can also create them manually:
```bash
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands}}
touch ~/.claude/homunculus/observations.jsonl
```
### 3. Use the Instinct Commands
```bash
/instinct-status # Show learned instincts with confidence scores
/evolve # Cluster related instincts into skills/commands
/instinct-export # Export instincts for sharing
/instinct-import # Import instincts from others
```
## Commands
| Command | Description |
|---------|-------------|
| `/instinct-status` | Show all learned instincts with confidence |
| `/evolve` | Cluster related instincts into skills/commands |
| `/instinct-export` | Export instincts for sharing |
| `/instinct-import <file>` | Import instincts from others |
## Configuration
Edit `config.json`:
```json
{
"version": "2.0",
"observation": {
"enabled": true,
"store_path": "~/.claude/homunculus/observations.jsonl",
"max_file_size_mb": 10,
"archive_after_days": 7
},
"instincts": {
"personal_path": "~/.claude/homunculus/instincts/personal/",
"inherited_path": "~/.claude/homunculus/instincts/inherited/",
"min_confidence": 0.3,
"auto_approve_threshold": 0.7,
"confidence_decay_rate": 0.05
},
"observer": {
"enabled": true,
"model": "haiku",
"run_interval_minutes": 5,
"patterns_to_detect": [
"user_corrections",
"error_resolutions",
"repeated_workflows",
"tool_preferences"
]
},
"evolution": {
"cluster_threshold": 3,
"evolved_path": "~/.claude/homunculus/evolved/"
}
}
```
## File Structure
```
~/.claude/homunculus/
├── identity.json # Your profile, technical level
├── observations.jsonl # Current session observations
├── observations.archive/ # Processed observations
├── instincts/
│ ├── personal/ # Auto-learned instincts
│ └── inherited/ # Imported from others
└── evolved/
├── agents/ # Generated specialist agents
├── skills/ # Generated skills
└── commands/ # Generated commands
```
## Integration with Skill Creator
When you use the [Skill Creator GitHub App](https://skill-creator.app), it now generates **both**:
- Traditional SKILL.md files (for backward compatibility)
- Instinct collections (for v2 learning system)
Instincts from repo analysis have `source: "repo-analysis"` and include the source repository URL.
## Confidence Scoring
Confidence evolves over time:
| Score | Meaning | Behavior |
|-------|---------|----------|
| 0.3 | Tentative | Suggested but not enforced |
| 0.5 | Moderate | Applied when relevant |
| 0.7 | Strong | Auto-approved for application |
| 0.9 | Near-certain | Core behavior |
**Confidence increases** when:
- Pattern is repeatedly observed
- User doesn't correct the suggested behavior
- Similar instincts from other sources agree
**Confidence decreases** when:
- User explicitly corrects the behavior
- Pattern isn't observed for extended periods
- Contradicting evidence appears
## Why Hooks vs Skills for Observation?
> "v1 relied on skills to observe. Skills are probabilistic—they fire ~50-80% of the time based on Claude's judgment."
Hooks fire **100% of the time**, deterministically. This means:
- Every tool call is observed
- No patterns are missed
- Learning is comprehensive
## Backward Compatibility
v2 is fully compatible with v1:
- Existing `~/.claude/skills/learned/` skills still work
- Stop hook still runs (but now also feeds into v2)
- Gradual migration path: run both in parallel
## Privacy
- Observations stay **local** on your machine
- Only **instincts** (patterns) can be exported
- No actual code or conversation content is shared
- You control what gets exported
## Related
- [Skill Creator](https://skill-creator.app) - Generate instincts from repo history
- [Homunculus](https://github.com/humanplane/homunculus) - Inspiration for v2 architecture
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - Continuous learning section
---
*Instinct-based learning: teaching Claude your patterns, one observation at a time.*

View File

@@ -0,0 +1,137 @@
---
name: observer
description: Background agent that analyzes session observations to detect patterns and create instincts. Uses Haiku for cost-efficiency.
model: haiku
run_mode: background
---
# Observer Agent
A background agent that analyzes observations from Claude Code sessions to detect patterns and create instincts.
## When to Run
- After significant session activity (20+ tool calls)
- When user runs `/analyze-patterns`
- On a scheduled interval (configurable, default 5 minutes)
- When triggered by observation hook (SIGUSR1)
## Input
Reads observations from `~/.claude/homunculus/observations.jsonl`:
```jsonl
{"timestamp":"2025-01-22T10:30:00Z","event":"tool_start","session":"abc123","tool":"Edit","input":"..."}
{"timestamp":"2025-01-22T10:30:01Z","event":"tool_complete","session":"abc123","tool":"Edit","output":"..."}
{"timestamp":"2025-01-22T10:30:05Z","event":"tool_start","session":"abc123","tool":"Bash","input":"npm test"}
{"timestamp":"2025-01-22T10:30:10Z","event":"tool_complete","session":"abc123","tool":"Bash","output":"All tests pass"}
```
## Pattern Detection
Look for these patterns in observations:
### 1. User Corrections
When a user's follow-up message corrects Claude's previous action:
- "No, use X instead of Y"
- "Actually, I meant..."
- Immediate undo/redo patterns
→ Create instinct: "When doing X, prefer Y"
### 2. Error Resolutions
When an error is followed by a fix:
- Tool output contains error
- Next few tool calls fix it
- Same error type resolved similarly multiple times
→ Create instinct: "When encountering error X, try Y"
### 3. Repeated Workflows
When the same sequence of tools is used multiple times:
- Same tool sequence with similar inputs
- File patterns that change together
- Time-clustered operations
→ Create workflow instinct: "When doing X, follow steps Y, Z, W"
### 4. Tool Preferences
When certain tools are consistently preferred:
- Always uses Grep before Edit
- Prefers Read over Bash cat
- Uses specific Bash commands for certain tasks
→ Create instinct: "When needing X, use tool Y"
## Output
Creates/updates instincts in `~/.claude/homunculus/instincts/personal/`:
```yaml
---
id: prefer-grep-before-edit
trigger: "when searching for code to modify"
confidence: 0.65
domain: "workflow"
source: "session-observation"
---
# Prefer Grep Before Edit
## Action
Always use Grep to find the exact location before using Edit.
## Evidence
- Observed 8 times in session abc123
- Pattern: Grep → Read → Edit sequence
- Last observed: 2025-01-22
```
## Confidence Calculation
Initial confidence based on observation frequency:
- 1-2 observations: 0.3 (tentative)
- 3-5 observations: 0.5 (moderate)
- 6-10 observations: 0.7 (strong)
- 11+ observations: 0.85 (very strong)
Confidence adjusts over time:
- +0.05 for each confirming observation
- -0.1 for each contradicting observation
- -0.02 per week without observation (decay)
## Important Guidelines
1. **Be Conservative**: Only create instincts for clear patterns (3+ observations)
2. **Be Specific**: Narrow triggers are better than broad ones
3. **Track Evidence**: Always include what observations led to the instinct
4. **Respect Privacy**: Never include actual code snippets, only patterns
5. **Merge Similar**: If a new instinct is similar to existing, update rather than duplicate
## Example Analysis Session
Given observations:
```jsonl
{"event":"tool_start","tool":"Grep","input":"pattern: useState"}
{"event":"tool_complete","tool":"Grep","output":"Found in 3 files"}
{"event":"tool_start","tool":"Read","input":"src/hooks/useAuth.ts"}
{"event":"tool_complete","tool":"Read","output":"[file content]"}
{"event":"tool_start","tool":"Edit","input":"src/hooks/useAuth.ts..."}
```
Analysis:
- Detected workflow: Grep → Read → Edit
- Frequency: Seen 5 times this session
- Create instinct:
- trigger: "when modifying code"
- action: "Search with Grep, confirm with Read, then Edit"
- confidence: 0.6
- domain: "workflow"
## Integration with Skill Creator
When instincts are imported from Skill Creator (repo analysis), they have:
- `source: "repo-analysis"`
- `source_repo: "https://github.com/..."`
These should be treated as team/project conventions with higher initial confidence (0.7+).

View File

@@ -0,0 +1,143 @@
#!/bin/bash
# Continuous Learning v2 - Observer Agent Launcher
#
# Starts the background observer agent that analyzes observations
# and creates instincts. Uses Haiku model for cost efficiency.
#
# Usage:
# start-observer.sh # Start observer in background
# start-observer.sh stop # Stop running observer
# start-observer.sh status # Check if observer is running
set -e
CONFIG_DIR="${HOME}/.claude/homunculus"
PID_FILE="${CONFIG_DIR}/.observer.pid"
LOG_FILE="${CONFIG_DIR}/observer.log"
OBSERVATIONS_FILE="${CONFIG_DIR}/observations.jsonl"
mkdir -p "$CONFIG_DIR"
case "${1:-start}" in
stop)
if [ -f "$PID_FILE" ]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "Stopping observer (PID: $pid)..."
kill "$pid"
rm -f "$PID_FILE"
echo "Observer stopped."
else
echo "Observer not running (stale PID file)."
rm -f "$PID_FILE"
fi
else
echo "Observer not running."
fi
exit 0
;;
status)
if [ -f "$PID_FILE" ]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "Observer is running (PID: $pid)"
echo "Log: $LOG_FILE"
echo "Observations: $(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0) lines"
exit 0
else
echo "Observer not running (stale PID file)"
rm -f "$PID_FILE"
exit 1
fi
else
echo "Observer not running"
exit 1
fi
;;
start)
# Check if already running
if [ -f "$PID_FILE" ]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "Observer already running (PID: $pid)"
exit 0
fi
rm -f "$PID_FILE"
fi
echo "Starting observer agent..."
# The observer loop
(
trap 'rm -f "$PID_FILE"; exit 0' TERM INT
analyze_observations() {
# Only analyze if observations file exists and has enough entries
if [ ! -f "$OBSERVATIONS_FILE" ]; then
return
fi
obs_count=$(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0)
if [ "$obs_count" -lt 10 ]; then
return
fi
echo "[$(date)] Analyzing $obs_count observations..." >> "$LOG_FILE"
# Use Claude Code with Haiku to analyze observations
# This spawns a quick analysis session
if command -v claude &> /dev/null; then
exit_code=0
claude --model haiku --max-turns 3 --print \
"Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \
>> "$LOG_FILE" 2>&1 || exit_code=$?
if [ "$exit_code" -ne 0 ]; then
echo "[$(date)] Claude analysis failed (exit $exit_code)" >> "$LOG_FILE"
fi
else
echo "[$(date)] claude CLI not found, skipping analysis" >> "$LOG_FILE"
fi
# Archive processed observations
if [ -f "$OBSERVATIONS_FILE" ]; then
archive_dir="${CONFIG_DIR}/observations.archive"
mkdir -p "$archive_dir"
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S).jsonl" 2>/dev/null || true
touch "$OBSERVATIONS_FILE"
fi
}
# Handle SIGUSR1 for on-demand analysis
trap 'analyze_observations' USR1
echo "$$" > "$PID_FILE"
echo "[$(date)] Observer started (PID: $$)" >> "$LOG_FILE"
while true; do
# Check every 5 minutes
sleep 300
analyze_observations
done
) &
disown
# Wait a moment for PID file
sleep 1
if [ -f "$PID_FILE" ]; then
echo "Observer started (PID: $(cat "$PID_FILE"))"
echo "Log: $LOG_FILE"
else
echo "Failed to start observer"
exit 1
fi
;;
*)
echo "Usage: $0 {start|stop|status}"
exit 1
;;
esac

View File

@@ -0,0 +1,41 @@
{
"version": "2.0",
"observation": {
"enabled": true,
"store_path": "~/.claude/homunculus/observations.jsonl",
"max_file_size_mb": 10,
"archive_after_days": 7,
"capture_tools": ["Edit", "Write", "Bash", "Read", "Grep", "Glob"],
"ignore_tools": ["TodoWrite"]
},
"instincts": {
"personal_path": "~/.claude/homunculus/instincts/personal/",
"inherited_path": "~/.claude/homunculus/instincts/inherited/",
"min_confidence": 0.3,
"auto_approve_threshold": 0.7,
"confidence_decay_rate": 0.02,
"max_instincts": 100
},
"observer": {
"enabled": false,
"model": "haiku",
"run_interval_minutes": 5,
"min_observations_to_analyze": 20,
"patterns_to_detect": [
"user_corrections",
"error_resolutions",
"repeated_workflows",
"tool_preferences",
"file_patterns"
]
},
"evolution": {
"cluster_threshold": 3,
"evolved_path": "~/.claude/homunculus/evolved/",
"auto_evolve": false
},
"integration": {
"skill_creator_api": "https://skill-creator.app/api",
"backward_compatible_v1": true
}
}

View File

@@ -0,0 +1,155 @@
#!/bin/bash
# Continuous Learning v2 - Observation Hook
#
# Captures tool use events for pattern analysis.
# Claude Code passes hook data via stdin as JSON.
#
# Hook config (in ~/.claude/settings.json):
#
# If installed as a plugin, use ${CLAUDE_PLUGIN_ROOT}:
# {
# "hooks": {
# "PreToolUse": [{
# "matcher": "*",
# "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh pre" }]
# }],
# "PostToolUse": [{
# "matcher": "*",
# "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh post" }]
# }]
# }
# }
#
# If installed manually to ~/.claude/skills:
# {
# "hooks": {
# "PreToolUse": [{
# "matcher": "*",
# "hooks": [{ "type": "command", "command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh pre" }]
# }],
# "PostToolUse": [{
# "matcher": "*",
# "hooks": [{ "type": "command", "command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh post" }]
# }]
# }
# }
set -e
CONFIG_DIR="${HOME}/.claude/homunculus"
OBSERVATIONS_FILE="${CONFIG_DIR}/observations.jsonl"
MAX_FILE_SIZE_MB=10
# Ensure directory exists
mkdir -p "$CONFIG_DIR"
# Skip if disabled
if [ -f "$CONFIG_DIR/disabled" ]; then
exit 0
fi
# Read JSON from stdin (Claude Code hook format)
INPUT_JSON=$(cat)
# Exit if no input
if [ -z "$INPUT_JSON" ]; then
exit 0
fi
# Parse using python via stdin pipe (safe for all JSON payloads)
PARSED=$(echo "$INPUT_JSON" | python3 -c '
import json
import sys
try:
data = json.load(sys.stdin)
# Extract fields - Claude Code hook format
hook_type = data.get("hook_type", "unknown") # PreToolUse or PostToolUse
tool_name = data.get("tool_name", data.get("tool", "unknown"))
tool_input = data.get("tool_input", data.get("input", {}))
tool_output = data.get("tool_output", data.get("output", ""))
session_id = data.get("session_id", "unknown")
# Truncate large inputs/outputs
if isinstance(tool_input, dict):
tool_input_str = json.dumps(tool_input)[:5000]
else:
tool_input_str = str(tool_input)[:5000]
if isinstance(tool_output, dict):
tool_output_str = json.dumps(tool_output)[:5000]
else:
tool_output_str = str(tool_output)[:5000]
# Determine event type
event = "tool_start" if "Pre" in hook_type else "tool_complete"
print(json.dumps({
"parsed": True,
"event": event,
"tool": tool_name,
"input": tool_input_str if event == "tool_start" else None,
"output": tool_output_str if event == "tool_complete" else None,
"session": session_id
}))
except Exception as e:
print(json.dumps({"parsed": False, "error": str(e)}))
')
# Check if parsing succeeded
PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.stdin).get('parsed', False))")
if [ "$PARSED_OK" != "True" ]; then
# Fallback: log raw input for debugging
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TIMESTAMP="$timestamp" echo "$INPUT_JSON" | python3 -c "
import json, sys, os
raw = sys.stdin.read()[:2000]
print(json.dumps({'timestamp': os.environ['TIMESTAMP'], 'event': 'parse_error', 'raw': raw}))
" >> "$OBSERVATIONS_FILE"
exit 0
fi
# Archive if file too large
if [ -f "$OBSERVATIONS_FILE" ]; then
file_size_mb=$(du -m "$OBSERVATIONS_FILE" 2>/dev/null | cut -f1)
if [ "${file_size_mb:-0}" -ge "$MAX_FILE_SIZE_MB" ]; then
archive_dir="${CONFIG_DIR}/observations.archive"
mkdir -p "$archive_dir"
mv "$OBSERVATIONS_FILE" "$archive_dir/observations-$(date +%Y%m%d-%H%M%S).jsonl"
fi
fi
# Build and write observation
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TIMESTAMP="$timestamp" echo "$PARSED" | python3 -c "
import json, sys, os
parsed = json.load(sys.stdin)
observation = {
'timestamp': os.environ['TIMESTAMP'],
'event': parsed['event'],
'tool': parsed['tool'],
'session': parsed['session']
}
if parsed['input']:
observation['input'] = parsed['input']
if parsed['output']:
observation['output'] = parsed['output']
print(json.dumps(observation))
" >> "$OBSERVATIONS_FILE"
# Signal observer if running
OBSERVER_PID_FILE="${CONFIG_DIR}/.observer.pid"
if [ -f "$OBSERVER_PID_FILE" ]; then
observer_pid=$(cat "$OBSERVER_PID_FILE")
if kill -0 "$observer_pid" 2>/dev/null; then
kill -USR1 "$observer_pid" 2>/dev/null || true
fi
fi
exit 0

View File

@@ -0,0 +1,575 @@
#!/usr/bin/env python3
"""
Instinct CLI - Manage instincts for Continuous Learning v2
Commands:
status - Show all instincts and their status
import - Import instincts from file or URL
export - Export instincts to file
evolve - Cluster instincts into skills/commands/agents
"""
import argparse
import json
import os
import sys
import re
import urllib.request
from pathlib import Path
from datetime import datetime
from collections import defaultdict
from typing import Optional
# ─────────────────────────────────────────────
# Configuration
# ─────────────────────────────────────────────
HOMUNCULUS_DIR = Path.home() / ".claude" / "homunculus"
INSTINCTS_DIR = HOMUNCULUS_DIR / "instincts"
PERSONAL_DIR = INSTINCTS_DIR / "personal"
INHERITED_DIR = INSTINCTS_DIR / "inherited"
EVOLVED_DIR = HOMUNCULUS_DIR / "evolved"
OBSERVATIONS_FILE = HOMUNCULUS_DIR / "observations.jsonl"
# Ensure directories exist
for d in [PERSONAL_DIR, INHERITED_DIR, EVOLVED_DIR / "skills", EVOLVED_DIR / "commands", EVOLVED_DIR / "agents"]:
d.mkdir(parents=True, exist_ok=True)
# ─────────────────────────────────────────────
# Instinct Parser
# ─────────────────────────────────────────────
def parse_instinct_file(content: str) -> list[dict]:
"""Parse YAML-like instinct file format."""
instincts = []
current = {}
in_frontmatter = False
content_lines = []
for line in content.split('\n'):
if line.strip() == '---':
if in_frontmatter:
# End of frontmatter - content comes next, don't append yet
in_frontmatter = False
else:
# Start of frontmatter
in_frontmatter = True
if current:
current['content'] = '\n'.join(content_lines).strip()
instincts.append(current)
current = {}
content_lines = []
elif in_frontmatter:
# Parse YAML-like frontmatter
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key == 'confidence':
current[key] = float(value)
else:
current[key] = value
else:
content_lines.append(line)
# Don't forget the last instinct
if current:
current['content'] = '\n'.join(content_lines).strip()
instincts.append(current)
return [i for i in instincts if i.get('id')]
def load_all_instincts() -> list[dict]:
"""Load all instincts from personal and inherited directories."""
instincts = []
for directory in [PERSONAL_DIR, INHERITED_DIR]:
if not directory.exists():
continue
yaml_files = sorted(
set(directory.glob("*.yaml"))
| set(directory.glob("*.yml"))
| set(directory.glob("*.md"))
)
for file in yaml_files:
try:
content = file.read_text()
parsed = parse_instinct_file(content)
for inst in parsed:
inst['_source_file'] = str(file)
inst['_source_type'] = directory.name
instincts.extend(parsed)
except Exception as e:
print(f"Warning: Failed to parse {file}: {e}", file=sys.stderr)
return instincts
# ─────────────────────────────────────────────
# Status Command
# ─────────────────────────────────────────────
def cmd_status(args):
"""Show status of all instincts."""
instincts = load_all_instincts()
if not instincts:
print("No instincts found.")
print(f"\nInstinct directories:")
print(f" Personal: {PERSONAL_DIR}")
print(f" Inherited: {INHERITED_DIR}")
return
# Group by domain
by_domain = defaultdict(list)
for inst in instincts:
domain = inst.get('domain', 'general')
by_domain[domain].append(inst)
# Print header
print(f"\n{'='*60}")
print(f" INSTINCT STATUS - {len(instincts)} total")
print(f"{'='*60}\n")
# Summary by source
personal = [i for i in instincts if i.get('_source_type') == 'personal']
inherited = [i for i in instincts if i.get('_source_type') == 'inherited']
print(f" Personal: {len(personal)}")
print(f" Inherited: {len(inherited)}")
print()
# Print by domain
for domain in sorted(by_domain.keys()):
domain_instincts = by_domain[domain]
print(f"## {domain.upper()} ({len(domain_instincts)})")
print()
for inst in sorted(domain_instincts, key=lambda x: -x.get('confidence', 0.5)):
conf = inst.get('confidence', 0.5)
conf_bar = '' * int(conf * 10) + '' * (10 - int(conf * 10))
trigger = inst.get('trigger', 'unknown trigger')
source = inst.get('source', 'unknown')
print(f" {conf_bar} {int(conf*100):3d}% {inst.get('id', 'unnamed')}")
print(f" trigger: {trigger}")
# Extract action from content
content = inst.get('content', '')
action_match = re.search(r'## Action\s*\n\s*(.+?)(?:\n\n|\n##|$)', content, re.DOTALL)
if action_match:
action = action_match.group(1).strip().split('\n')[0]
print(f" action: {action[:60]}{'...' if len(action) > 60 else ''}")
print()
# Observations stats
if OBSERVATIONS_FILE.exists():
obs_count = sum(1 for _ in open(OBSERVATIONS_FILE))
print(f"─────────────────────────────────────────────────────────")
print(f" Observations: {obs_count} events logged")
print(f" File: {OBSERVATIONS_FILE}")
print(f"\n{'='*60}\n")
# ─────────────────────────────────────────────
# Import Command
# ─────────────────────────────────────────────
def cmd_import(args):
"""Import instincts from file or URL."""
source = args.source
# Fetch content
if source.startswith('http://') or source.startswith('https://'):
print(f"Fetching from URL: {source}")
try:
with urllib.request.urlopen(source) as response:
content = response.read().decode('utf-8')
except Exception as e:
print(f"Error fetching URL: {e}", file=sys.stderr)
return 1
else:
path = Path(source).expanduser()
if not path.exists():
print(f"File not found: {path}", file=sys.stderr)
return 1
content = path.read_text()
# Parse instincts
new_instincts = parse_instinct_file(content)
if not new_instincts:
print("No valid instincts found in source.")
return 1
print(f"\nFound {len(new_instincts)} instincts to import.\n")
# Load existing
existing = load_all_instincts()
existing_ids = {i.get('id') for i in existing}
# Categorize
to_add = []
duplicates = []
to_update = []
for inst in new_instincts:
inst_id = inst.get('id')
if inst_id in existing_ids:
# Check if we should update
existing_inst = next((e for e in existing if e.get('id') == inst_id), None)
if existing_inst:
if inst.get('confidence', 0) > existing_inst.get('confidence', 0):
to_update.append(inst)
else:
duplicates.append(inst)
else:
to_add.append(inst)
# Filter by minimum confidence
min_conf = args.min_confidence or 0.0
to_add = [i for i in to_add if i.get('confidence', 0.5) >= min_conf]
to_update = [i for i in to_update if i.get('confidence', 0.5) >= min_conf]
# Display summary
if to_add:
print(f"NEW ({len(to_add)}):")
for inst in to_add:
print(f" + {inst.get('id')} (confidence: {inst.get('confidence', 0.5):.2f})")
if to_update:
print(f"\nUPDATE ({len(to_update)}):")
for inst in to_update:
print(f" ~ {inst.get('id')} (confidence: {inst.get('confidence', 0.5):.2f})")
if duplicates:
print(f"\nSKIP ({len(duplicates)} - already exists with equal/higher confidence):")
for inst in duplicates[:5]:
print(f" - {inst.get('id')}")
if len(duplicates) > 5:
print(f" ... and {len(duplicates) - 5} more")
if args.dry_run:
print("\n[DRY RUN] No changes made.")
return 0
if not to_add and not to_update:
print("\nNothing to import.")
return 0
# Confirm
if not args.force:
response = input(f"\nImport {len(to_add)} new, update {len(to_update)}? [y/N] ")
if response.lower() != 'y':
print("Cancelled.")
return 0
# Write to inherited directory
timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')
source_name = Path(source).stem if not source.startswith('http') else 'web-import'
output_file = INHERITED_DIR / f"{source_name}-{timestamp}.yaml"
all_to_write = to_add + to_update
output_content = f"# Imported from {source}\n# Date: {datetime.now().isoformat()}\n\n"
for inst in all_to_write:
output_content += "---\n"
output_content += f"id: {inst.get('id')}\n"
output_content += f"trigger: \"{inst.get('trigger', 'unknown')}\"\n"
output_content += f"confidence: {inst.get('confidence', 0.5)}\n"
output_content += f"domain: {inst.get('domain', 'general')}\n"
output_content += f"source: inherited\n"
output_content += f"imported_from: \"{source}\"\n"
if inst.get('source_repo'):
output_content += f"source_repo: {inst.get('source_repo')}\n"
output_content += "---\n\n"
output_content += inst.get('content', '') + "\n\n"
output_file.write_text(output_content)
print(f"\n✅ Import complete!")
print(f" Added: {len(to_add)}")
print(f" Updated: {len(to_update)}")
print(f" Saved to: {output_file}")
return 0
# ─────────────────────────────────────────────
# Export Command
# ─────────────────────────────────────────────
def cmd_export(args):
"""Export instincts to file."""
instincts = load_all_instincts()
if not instincts:
print("No instincts to export.")
return 1
# Filter by domain if specified
if args.domain:
instincts = [i for i in instincts if i.get('domain') == args.domain]
# Filter by minimum confidence
if args.min_confidence:
instincts = [i for i in instincts if i.get('confidence', 0.5) >= args.min_confidence]
if not instincts:
print("No instincts match the criteria.")
return 1
# Generate output
output = f"# Instincts export\n# Date: {datetime.now().isoformat()}\n# Total: {len(instincts)}\n\n"
for inst in instincts:
output += "---\n"
for key in ['id', 'trigger', 'confidence', 'domain', 'source', 'source_repo']:
if inst.get(key):
value = inst[key]
if key == 'trigger':
output += f'{key}: "{value}"\n'
else:
output += f"{key}: {value}\n"
output += "---\n\n"
output += inst.get('content', '') + "\n\n"
# Write to file or stdout
if args.output:
Path(args.output).write_text(output)
print(f"Exported {len(instincts)} instincts to {args.output}")
else:
print(output)
return 0
# ─────────────────────────────────────────────
# Evolve Command
# ─────────────────────────────────────────────
def cmd_evolve(args):
"""Analyze instincts and suggest evolutions to skills/commands/agents."""
instincts = load_all_instincts()
if len(instincts) < 3:
print("Need at least 3 instincts to analyze patterns.")
print(f"Currently have: {len(instincts)}")
return 1
print(f"\n{'='*60}")
print(f" EVOLVE ANALYSIS - {len(instincts)} instincts")
print(f"{'='*60}\n")
# Group by domain
by_domain = defaultdict(list)
for inst in instincts:
domain = inst.get('domain', 'general')
by_domain[domain].append(inst)
# High-confidence instincts by domain (candidates for skills)
high_conf = [i for i in instincts if i.get('confidence', 0) >= 0.8]
print(f"High confidence instincts (>=80%): {len(high_conf)}")
# Find clusters (instincts with similar triggers)
trigger_clusters = defaultdict(list)
for inst in instincts:
trigger = inst.get('trigger', '')
# Normalize trigger
trigger_key = trigger.lower()
for keyword in ['when', 'creating', 'writing', 'adding', 'implementing', 'testing']:
trigger_key = trigger_key.replace(keyword, '').strip()
trigger_clusters[trigger_key].append(inst)
# Find clusters with 3+ instincts (good skill candidates)
skill_candidates = []
for trigger, cluster in trigger_clusters.items():
if len(cluster) >= 2:
avg_conf = sum(i.get('confidence', 0.5) for i in cluster) / len(cluster)
skill_candidates.append({
'trigger': trigger,
'instincts': cluster,
'avg_confidence': avg_conf,
'domains': list(set(i.get('domain', 'general') for i in cluster))
})
# Sort by cluster size and confidence
skill_candidates.sort(key=lambda x: (-len(x['instincts']), -x['avg_confidence']))
print(f"\nPotential skill clusters found: {len(skill_candidates)}")
if skill_candidates:
print(f"\n## SKILL CANDIDATES\n")
for i, cand in enumerate(skill_candidates[:5], 1):
print(f"{i}. Cluster: \"{cand['trigger']}\"")
print(f" Instincts: {len(cand['instincts'])}")
print(f" Avg confidence: {cand['avg_confidence']:.0%}")
print(f" Domains: {', '.join(cand['domains'])}")
print(f" Instincts:")
for inst in cand['instincts'][:3]:
print(f" - {inst.get('id')}")
print()
# Command candidates (workflow instincts with high confidence)
workflow_instincts = [i for i in instincts if i.get('domain') == 'workflow' and i.get('confidence', 0) >= 0.7]
if workflow_instincts:
print(f"\n## COMMAND CANDIDATES ({len(workflow_instincts)})\n")
for inst in workflow_instincts[:5]:
trigger = inst.get('trigger', 'unknown')
# Suggest command name
cmd_name = trigger.replace('when ', '').replace('implementing ', '').replace('a ', '')
cmd_name = cmd_name.replace(' ', '-')[:20]
print(f" /{cmd_name}")
print(f" From: {inst.get('id')}")
print(f" Confidence: {inst.get('confidence', 0.5):.0%}")
print()
# Agent candidates (complex multi-step patterns)
agent_candidates = [c for c in skill_candidates if len(c['instincts']) >= 3 and c['avg_confidence'] >= 0.75]
if agent_candidates:
print(f"\n## AGENT CANDIDATES ({len(agent_candidates)})\n")
for cand in agent_candidates[:3]:
agent_name = cand['trigger'].replace(' ', '-')[:20] + '-agent'
print(f" {agent_name}")
print(f" Covers {len(cand['instincts'])} instincts")
print(f" Avg confidence: {cand['avg_confidence']:.0%}")
print()
if args.generate:
generated = _generate_evolved(skill_candidates, workflow_instincts, agent_candidates)
if generated:
print(f"\n✅ Generated {len(generated)} evolved structures:")
for path in generated:
print(f" {path}")
else:
print("\nNo structures generated (need higher-confidence clusters).")
print(f"\n{'='*60}\n")
return 0
# ─────────────────────────────────────────────
# Generate Evolved Structures
# ─────────────────────────────────────────────
def _generate_evolved(skill_candidates: list, workflow_instincts: list, agent_candidates: list) -> list[str]:
"""Generate skill/command/agent files from analyzed instinct clusters."""
generated = []
# Generate skills from top candidates
for cand in skill_candidates[:5]:
trigger = cand['trigger'].strip()
if not trigger:
continue
name = re.sub(r'[^a-z0-9]+', '-', trigger.lower()).strip('-')[:30]
if not name:
continue
skill_dir = EVOLVED_DIR / "skills" / name
skill_dir.mkdir(parents=True, exist_ok=True)
content = f"# {name}\n\n"
content += f"Evolved from {len(cand['instincts'])} instincts "
content += f"(avg confidence: {cand['avg_confidence']:.0%})\n\n"
content += f"## When to Apply\n\n"
content += f"Trigger: {trigger}\n\n"
content += f"## Actions\n\n"
for inst in cand['instincts']:
inst_content = inst.get('content', '')
action_match = re.search(r'## Action\s*\n\s*(.+?)(?:\n\n|\n##|$)', inst_content, re.DOTALL)
action = action_match.group(1).strip() if action_match else inst.get('id', 'unnamed')
content += f"- {action}\n"
(skill_dir / "SKILL.md").write_text(content)
generated.append(str(skill_dir / "SKILL.md"))
# Generate commands from workflow instincts
for inst in workflow_instincts[:5]:
trigger = inst.get('trigger', 'unknown')
cmd_name = re.sub(r'[^a-z0-9]+', '-', trigger.lower().replace('when ', '').replace('implementing ', ''))
cmd_name = cmd_name.strip('-')[:20]
if not cmd_name:
continue
cmd_file = EVOLVED_DIR / "commands" / f"{cmd_name}.md"
content = f"# {cmd_name}\n\n"
content += f"Evolved from instinct: {inst.get('id', 'unnamed')}\n"
content += f"Confidence: {inst.get('confidence', 0.5):.0%}\n\n"
content += inst.get('content', '')
cmd_file.write_text(content)
generated.append(str(cmd_file))
# Generate agents from complex clusters
for cand in agent_candidates[:3]:
trigger = cand['trigger'].strip()
agent_name = re.sub(r'[^a-z0-9]+', '-', trigger.lower()).strip('-')[:20]
if not agent_name:
continue
agent_file = EVOLVED_DIR / "agents" / f"{agent_name}.md"
domains = ', '.join(cand['domains'])
instinct_ids = [i.get('id', 'unnamed') for i in cand['instincts']]
content = f"---\nmodel: sonnet\ntools: Read, Grep, Glob\n---\n"
content += f"# {agent_name}\n\n"
content += f"Evolved from {len(cand['instincts'])} instincts "
content += f"(avg confidence: {cand['avg_confidence']:.0%})\n"
content += f"Domains: {domains}\n\n"
content += f"## Source Instincts\n\n"
for iid in instinct_ids:
content += f"- {iid}\n"
agent_file.write_text(content)
generated.append(str(agent_file))
return generated
# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description='Instinct CLI for Continuous Learning v2')
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Status
status_parser = subparsers.add_parser('status', help='Show instinct status')
# Import
import_parser = subparsers.add_parser('import', help='Import instincts')
import_parser.add_argument('source', help='File path or URL')
import_parser.add_argument('--dry-run', action='store_true', help='Preview without importing')
import_parser.add_argument('--force', action='store_true', help='Skip confirmation')
import_parser.add_argument('--min-confidence', type=float, help='Minimum confidence threshold')
# Export
export_parser = subparsers.add_parser('export', help='Export instincts')
export_parser.add_argument('--output', '-o', help='Output file')
export_parser.add_argument('--domain', help='Filter by domain')
export_parser.add_argument('--min-confidence', type=float, help='Minimum confidence')
# Evolve
evolve_parser = subparsers.add_parser('evolve', help='Analyze and evolve instincts')
evolve_parser.add_argument('--generate', action='store_true', help='Generate evolved structures')
args = parser.parse_args()
if args.command == 'status':
return cmd_status(args)
elif args.command == 'import':
return cmd_import(args)
elif args.command == 'export':
return cmd_export(args)
elif args.command == 'evolve':
return cmd_evolve(args)
else:
parser.print_help()
return 1
if __name__ == '__main__':
sys.exit(main() or 0)

View File

@@ -0,0 +1,82 @@
"""Tests for parse_instinct_file() — verifies content after frontmatter is preserved."""
import importlib.util
import os
# Load instinct-cli.py (hyphenated filename requires importlib)
_spec = importlib.util.spec_from_file_location(
"instinct_cli",
os.path.join(os.path.dirname(__file__), "instinct-cli.py"),
)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
parse_instinct_file = _mod.parse_instinct_file
MULTI_SECTION = """\
---
id: instinct-a
trigger: "when coding"
confidence: 0.9
domain: general
---
## Action
Do thing A.
## Examples
- Example A1
---
id: instinct-b
trigger: "when testing"
confidence: 0.7
domain: testing
---
## Action
Do thing B.
"""
def test_multiple_instincts_preserve_content():
result = parse_instinct_file(MULTI_SECTION)
assert len(result) == 2
assert "Do thing A." in result[0]["content"]
assert "Example A1" in result[0]["content"]
assert "Do thing B." in result[1]["content"]
def test_single_instinct_preserves_content():
content = """\
---
id: solo
trigger: "when reviewing"
confidence: 0.8
domain: review
---
## Action
Check for security issues.
## Evidence
Prevents vulnerabilities.
"""
result = parse_instinct_file(content)
assert len(result) == 1
assert "Check for security issues." in result[0]["content"]
assert "Prevents vulnerabilities." in result[0]["content"]
def test_empty_content_no_error():
content = """\
---
id: empty
trigger: "placeholder"
confidence: 0.5
domain: general
---
"""
result = parse_instinct_file(content)
assert len(result) == 1
assert result[0]["content"] == ""