Initial: Claude config with agents, skills, commands, rules and scripts
This commit is contained in:
292
skills/continuous-learning-v2/SKILL.md
Normal file
292
skills/continuous-learning-v2/SKILL.md
Normal 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.*
|
||||
137
skills/continuous-learning-v2/agents/observer.md
Normal file
137
skills/continuous-learning-v2/agents/observer.md
Normal 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+).
|
||||
143
skills/continuous-learning-v2/agents/start-observer.sh
Executable file
143
skills/continuous-learning-v2/agents/start-observer.sh
Executable 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
|
||||
41
skills/continuous-learning-v2/config.json
Normal file
41
skills/continuous-learning-v2/config.json
Normal 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
|
||||
}
|
||||
}
|
||||
155
skills/continuous-learning-v2/hooks/observe.sh
Executable file
155
skills/continuous-learning-v2/hooks/observe.sh
Executable 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
|
||||
575
skills/continuous-learning-v2/scripts/instinct-cli.py
Executable file
575
skills/continuous-learning-v2/scripts/instinct-cli.py
Executable 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)
|
||||
82
skills/continuous-learning-v2/scripts/test_parse_instinct.py
Normal file
82
skills/continuous-learning-v2/scripts/test_parse_instinct.py
Normal 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"] == ""
|
||||
Reference in New Issue
Block a user