mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-16 15:13:33 +08:00
Compare commits
6 Commits
v1.7.0
...
5818e8adc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5818e8adc7 | ||
|
|
2d3be88bb5 | ||
|
|
87a2ed51dc | ||
|
|
b68558d749 | ||
|
|
1fa22efd90 | ||
|
|
dc8455dd10 |
15
.github/FUNDING.yml
vendored
15
.github/FUNDING.yml
vendored
@@ -1,15 +1,2 @@
|
|||||||
# These are supported funding model platforms
|
github: affaan-m
|
||||||
|
|
||||||
github: [affaan-m]
|
|
||||||
# patreon: # Replace with a single Patreon username
|
|
||||||
# open_collective: # Replace with a single Open Collective username
|
|
||||||
# ko_fi: # Replace with a single Ko-fi username
|
|
||||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-hierarchical-namespace-controller
|
|
||||||
# liberapay: # Replace with a single Liberapay username
|
|
||||||
# issuehunt: # Replace with a single IssueHunt username
|
|
||||||
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-hierarchical-namespace-controller
|
|
||||||
# polar: # Replace with a single Polar username
|
|
||||||
# buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
||||||
# thanks_dev: # Replace with a single thanks.dev username
|
|
||||||
custom: ['https://ecc.tools']
|
custom: ['https://ecc.tools']
|
||||||
|
|||||||
48
.github/workflows/copilot-setup-steps.yml
vendored
48
.github/workflows/copilot-setup-steps.yml
vendored
@@ -1,18 +1,30 @@
|
|||||||
steps:
|
name: Copilot Setup Steps
|
||||||
- name: Setup Go environment
|
|
||||||
uses: actions/setup-go@v6.2.0
|
on:
|
||||||
with:
|
workflow_dispatch:
|
||||||
# The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks.
|
|
||||||
go-version: # optional
|
permissions:
|
||||||
# Path to the go.mod, go.work, .go-version, or .tool-versions file.
|
contents: read
|
||||||
go-version-file: # optional
|
|
||||||
# Set this option to true if you want the action to always check for the latest available version that satisfies the version spec
|
jobs:
|
||||||
check-latest: # optional
|
copilot-setup-steps:
|
||||||
# Used to pull Go distributions from go-versions. Since there's a default, this is typically not supplied by the user. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
|
runs-on: ubuntu-latest
|
||||||
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
|
timeout-minutes: 10
|
||||||
# Used to specify whether caching is needed. Set to true, if you'd like to enable caching.
|
steps:
|
||||||
cache: # optional, default is true
|
- name: Checkout repository
|
||||||
# Used to specify the path to a dependency file - go.sum
|
uses: actions/checkout@v4
|
||||||
cache-dependency-path: # optional
|
|
||||||
# Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default.
|
- name: Setup Node.js
|
||||||
architecture: # optional
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Verify environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
python3 --version
|
||||||
|
|||||||
@@ -258,6 +258,8 @@ After migration, ALL 23 commands are available:
|
|||||||
| `/instinct-import` | Import instincts |
|
| `/instinct-import` | Import instincts |
|
||||||
| `/instinct-export` | Export instincts |
|
| `/instinct-export` | Export instincts |
|
||||||
| `/evolve` | Cluster instincts into skills |
|
| `/evolve` | Cluster instincts into skills |
|
||||||
|
| `/promote` | Promote project instincts to global scope |
|
||||||
|
| `/projects` | List known projects and instinct stats |
|
||||||
|
|
||||||
## Available Agents
|
## Available Agents
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ opencode
|
|||||||
| `/instinct-import` | Import instincts |
|
| `/instinct-import` | Import instincts |
|
||||||
| `/instinct-export` | Export instincts |
|
| `/instinct-export` | Export instincts |
|
||||||
| `/evolve` | Cluster instincts |
|
| `/evolve` | Cluster instincts |
|
||||||
|
| `/promote` | Promote project instincts |
|
||||||
|
| `/projects` | List known projects |
|
||||||
|
|
||||||
### Plugin Hooks
|
### Plugin Hooks
|
||||||
|
|
||||||
|
|||||||
@@ -1,112 +1,36 @@
|
|||||||
---
|
---
|
||||||
description: Cluster instincts into skills
|
description: Analyze instincts and suggest or generate evolved structures
|
||||||
agent: build
|
agent: build
|
||||||
---
|
---
|
||||||
|
|
||||||
# Evolve Command
|
# Evolve Command
|
||||||
|
|
||||||
Cluster related instincts into structured skills: $ARGUMENTS
|
Analyze and evolve instincts in continuous-learning-v2: $ARGUMENTS
|
||||||
|
|
||||||
## Your Task
|
## Your Task
|
||||||
|
|
||||||
Analyze instincts and promote clusters to skills.
|
Run:
|
||||||
|
|
||||||
## Evolution Process
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" evolve $ARGUMENTS
|
||||||
### Step 1: Analyze Instincts
|
|
||||||
|
|
||||||
Group instincts by:
|
|
||||||
- Trigger similarity
|
|
||||||
- Action patterns
|
|
||||||
- Category tags
|
|
||||||
- Confidence levels
|
|
||||||
|
|
||||||
### Step 2: Identify Clusters
|
|
||||||
|
|
||||||
```
|
|
||||||
Cluster: Error Handling
|
|
||||||
├── Instinct: Catch specific errors (0.85)
|
|
||||||
├── Instinct: Wrap errors with context (0.82)
|
|
||||||
├── Instinct: Log errors with stack trace (0.78)
|
|
||||||
└── Instinct: Return meaningful error messages (0.80)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Generate Skill
|
If `CLAUDE_PLUGIN_ROOT` is unavailable, use:
|
||||||
|
|
||||||
When cluster has:
|
```bash
|
||||||
- 3+ instincts
|
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py evolve $ARGUMENTS
|
||||||
- Average confidence > 0.75
|
|
||||||
- Cohesive theme
|
|
||||||
|
|
||||||
Generate SKILL.md:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Error Handling Skill
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Patterns for robust error handling learned from session observations.
|
|
||||||
|
|
||||||
## Patterns
|
|
||||||
|
|
||||||
### 1. Catch Specific Errors
|
|
||||||
**Trigger**: When catching errors with generic catch
|
|
||||||
**Action**: Use specific error types
|
|
||||||
|
|
||||||
### 2. Wrap Errors with Context
|
|
||||||
**Trigger**: When re-throwing errors
|
|
||||||
**Action**: Add context with fmt.Errorf or Error.cause
|
|
||||||
|
|
||||||
### 3. Log with Stack Trace
|
|
||||||
**Trigger**: When logging errors
|
|
||||||
**Action**: Include stack trace for debugging
|
|
||||||
|
|
||||||
### 4. Meaningful Messages
|
|
||||||
**Trigger**: When returning errors to users
|
|
||||||
**Action**: Provide actionable error messages
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Archive Instincts
|
## Supported Args (v2.1)
|
||||||
|
|
||||||
Move evolved instincts to `archived/` with reference to skill.
|
- no args: analysis only
|
||||||
|
- `--generate`: also generate files under `evolved/{skills,commands,agents}`
|
||||||
|
|
||||||
## Evolution Report
|
## Behavior Notes
|
||||||
|
|
||||||
```
|
- Uses project + global instincts for analysis.
|
||||||
Evolution Summary
|
- Shows skill/command/agent candidates from trigger and domain clustering.
|
||||||
=================
|
- Shows project -> global promotion candidates.
|
||||||
|
- With `--generate`, output path is:
|
||||||
Clusters Found: X
|
- project context: `~/.claude/homunculus/projects/<project-id>/evolved/`
|
||||||
|
- global fallback: `~/.claude/homunculus/evolved/`
|
||||||
Cluster 1: Error Handling
|
|
||||||
- Instincts: 5
|
|
||||||
- Avg Confidence: 0.82
|
|
||||||
- Status: ✅ Promoted to skill
|
|
||||||
|
|
||||||
Cluster 2: Testing Patterns
|
|
||||||
- Instincts: 3
|
|
||||||
- Avg Confidence: 0.71
|
|
||||||
- Status: ⏳ Needs more confidence
|
|
||||||
|
|
||||||
Cluster 3: Git Workflow
|
|
||||||
- Instincts: 2
|
|
||||||
- Avg Confidence: 0.88
|
|
||||||
- Status: ⏳ Needs more instincts
|
|
||||||
|
|
||||||
Skills Created:
|
|
||||||
- skills/error-handling/SKILL.md
|
|
||||||
|
|
||||||
Instincts Archived: 5
|
|
||||||
Remaining Instincts: 12
|
|
||||||
```
|
|
||||||
|
|
||||||
## Thresholds
|
|
||||||
|
|
||||||
| Metric | Threshold |
|
|
||||||
|--------|-----------|
|
|
||||||
| Min instincts per cluster | 3 |
|
|
||||||
| Min average confidence | 0.75 |
|
|
||||||
| Min cluster cohesion | 0.6 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**TIP**: Run `/evolve` periodically to graduate instincts to skills as confidence grows.
|
|
||||||
|
|||||||
@@ -1,75 +1,29 @@
|
|||||||
---
|
---
|
||||||
description: View learned instincts with confidence scores
|
description: Show learned instincts (project + global) with confidence
|
||||||
agent: build
|
agent: build
|
||||||
---
|
---
|
||||||
|
|
||||||
# Instinct Status Command
|
# Instinct Status Command
|
||||||
|
|
||||||
Display learned instincts and their confidence scores: $ARGUMENTS
|
Show instinct status from continuous-learning-v2: $ARGUMENTS
|
||||||
|
|
||||||
## Your Task
|
## Your Task
|
||||||
|
|
||||||
Read and display instincts from the continuous-learning-v2 system.
|
Run:
|
||||||
|
|
||||||
## Instinct Location
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" status
|
||||||
Global: `~/.claude/instincts/`
|
|
||||||
Project: `.claude/instincts/`
|
|
||||||
|
|
||||||
## Status Display
|
|
||||||
|
|
||||||
### Instinct Summary
|
|
||||||
|
|
||||||
| Category | Count | Avg Confidence |
|
|
||||||
|----------|-------|----------------|
|
|
||||||
| Coding | X | 0.XX |
|
|
||||||
| Testing | X | 0.XX |
|
|
||||||
| Security | X | 0.XX |
|
|
||||||
| Git | X | 0.XX |
|
|
||||||
|
|
||||||
### High Confidence Instincts (>0.8)
|
|
||||||
|
|
||||||
```
|
|
||||||
[trigger] → [action] (confidence: 0.XX)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Learning Progress
|
If `CLAUDE_PLUGIN_ROOT` is unavailable, use:
|
||||||
|
|
||||||
- Total instincts: X
|
```bash
|
||||||
- This session: X
|
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status
|
||||||
- Promoted to skills: X
|
|
||||||
|
|
||||||
### Recent Instincts
|
|
||||||
|
|
||||||
Last 5 instincts learned:
|
|
||||||
|
|
||||||
1. **[timestamp]** - [trigger] → [action]
|
|
||||||
2. **[timestamp]** - [trigger] → [action]
|
|
||||||
...
|
|
||||||
|
|
||||||
## Instinct Structure
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "instinct-123",
|
|
||||||
"trigger": "When I see a try-catch without specific error type",
|
|
||||||
"action": "Suggest using specific error types for better handling",
|
|
||||||
"confidence": 0.75,
|
|
||||||
"applications": 5,
|
|
||||||
"successes": 4,
|
|
||||||
"source": "session-observation",
|
|
||||||
"timestamp": "2025-01-15T10:30:00Z"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Confidence Calculation
|
## Behavior Notes
|
||||||
|
|
||||||
```
|
- Output includes both project-scoped and global instincts.
|
||||||
confidence = (successes + 1) / (applications + 2)
|
- Project instincts override global instincts when IDs conflict.
|
||||||
```
|
- Output is grouped by domain with confidence bars.
|
||||||
|
- This command does not support extra filters in v2.1.
|
||||||
Bayesian smoothing ensures new instincts don't have extreme confidence.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**TIP**: Use `/evolve` to cluster related instincts into skills when confidence is high.
|
|
||||||
|
|||||||
23
.opencode/commands/projects.md
Normal file
23
.opencode/commands/projects.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
description: List registered projects and instinct counts
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# Projects Command
|
||||||
|
|
||||||
|
Show continuous-learning-v2 project registry and stats: $ARGUMENTS
|
||||||
|
|
||||||
|
## Your Task
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" projects
|
||||||
|
```
|
||||||
|
|
||||||
|
If `CLAUDE_PLUGIN_ROOT` is unavailable, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py projects
|
||||||
|
```
|
||||||
|
|
||||||
23
.opencode/commands/promote.md
Normal file
23
.opencode/commands/promote.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
description: Promote project instincts to global scope
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# Promote Command
|
||||||
|
|
||||||
|
Promote instincts in continuous-learning-v2: $ARGUMENTS
|
||||||
|
|
||||||
|
## Your Task
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" promote $ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
If `CLAUDE_PLUGIN_ROOT` is unavailable, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py promote $ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
@@ -303,6 +303,14 @@
|
|||||||
"evolve": {
|
"evolve": {
|
||||||
"description": "Cluster instincts into skills",
|
"description": "Cluster instincts into skills",
|
||||||
"template": "{file:.opencode/commands/evolve.md}\n\n$ARGUMENTS"
|
"template": "{file:.opencode/commands/evolve.md}\n\n$ARGUMENTS"
|
||||||
|
},
|
||||||
|
"promote": {
|
||||||
|
"description": "Promote project instincts to global scope",
|
||||||
|
"template": "{file:.opencode/commands/promote.md}\n\n$ARGUMENTS"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"description": "List known projects and instinct stats",
|
||||||
|
"template": "{file:.opencode/commands/projects.md}\n\n$ARGUMENTS"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -27,9 +27,11 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**The complete collection of Claude Code configs from an Anthropic hackathon winner.**
|
**The performance optimization system for AI agent harnesses. From an Anthropic hackathon winner.**
|
||||||
|
|
||||||
Production-ready agents, skills, hooks, commands, rules, and MCP configurations evolved over 10+ months of intensive daily use building real products.
|
Not just configs. A complete system: skills, instincts, memory optimization, continuous learning, security scanning, and research-first development. Production-ready agents, hooks, commands, rules, and MCP configurations evolved over 10+ months of intensive daily use building real products.
|
||||||
|
|
||||||
|
Works across **Claude Code**, **Codex**, **Cowork**, and other AI agent harnesses.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -440,6 +442,10 @@ Use `/security-scan` in Claude Code to run it, or add to CI with the [GitHub Act
|
|||||||
|
|
||||||
[GitHub](https://github.com/affaan-m/agentshield) | [npm](https://www.npmjs.com/package/ecc-agentshield)
|
[GitHub](https://github.com/affaan-m/agentshield) | [npm](https://www.npmjs.com/package/ecc-agentshield)
|
||||||
|
|
||||||
|
### 🔬 Plankton — Code Quality Integration
|
||||||
|
|
||||||
|
[Plankton](https://github.com/alexfazio/plankton) is a recommended companion for code quality enforcement. It provides automated code review, linting orchestration, and quality gates that pair well with the ECC skill and hook system. Use it alongside AgentShield for security + quality coverage.
|
||||||
|
|
||||||
### 🧠 Continuous Learning v2
|
### 🧠 Continuous Learning v2
|
||||||
|
|
||||||
The instinct-based learning system automatically learns your patterns:
|
The instinct-based learning system automatically learns your patterns:
|
||||||
@@ -557,8 +563,15 @@ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
|||||||
# Copy commands
|
# Copy commands
|
||||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||||
|
|
||||||
# Copy skills
|
# Copy skills (core vs niche)
|
||||||
cp -r everything-claude-code/skills/* ~/.claude/skills/
|
# Recommended (new users): core/general skills only
|
||||||
|
cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/
|
||||||
|
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
||||||
|
|
||||||
|
# Optional: add niche/framework-specific skills only when needed
|
||||||
|
# for s in django-patterns django-tdd springboot-patterns; do
|
||||||
|
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
||||||
|
# done
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add hooks to settings.json
|
#### Add hooks to settings.json
|
||||||
@@ -972,6 +985,8 @@ OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ event t
|
|||||||
| `/instinct-import` | Import instincts |
|
| `/instinct-import` | Import instincts |
|
||||||
| `/instinct-export` | Export instincts |
|
| `/instinct-export` | Export instincts |
|
||||||
| `/evolve` | Cluster instincts into skills |
|
| `/evolve` | Cluster instincts into skills |
|
||||||
|
| `/promote` | Promote project instincts to global scope |
|
||||||
|
| `/projects` | List known projects and instinct stats |
|
||||||
| `/learn-eval` | Extract and evaluate patterns before saving |
|
| `/learn-eval` | Extract and evaluate patterns before saving |
|
||||||
| `/setup-pm` | Configure package manager |
|
| `/setup-pm` | Configure package manager |
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,8 @@ everything-claude-code/
|
|||||||
/instinct-import <file> # 从他人导入直觉
|
/instinct-import <file> # 从他人导入直觉
|
||||||
/instinct-export # 导出你的直觉以供分享
|
/instinct-export # 导出你的直觉以供分享
|
||||||
/evolve # 将相关直觉聚类到技能中
|
/evolve # 将相关直觉聚类到技能中
|
||||||
|
/promote # 将项目级直觉提升为全局直觉
|
||||||
|
/projects # 查看已识别项目与直觉统计
|
||||||
```
|
```
|
||||||
|
|
||||||
完整文档见 `skills/continuous-learning-v2/`。
|
完整文档见 `skills/continuous-learning-v2/`。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: evolve
|
name: evolve
|
||||||
description: Cluster related instincts into skills, commands, or agents
|
description: Analyze instincts and suggest or generate evolved structures
|
||||||
command: true
|
command: true
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,9 +29,7 @@ Analyzes instincts and clusters related ones into higher-level structures:
|
|||||||
|
|
||||||
```
|
```
|
||||||
/evolve # Analyze all instincts and suggest evolutions
|
/evolve # Analyze all instincts and suggest evolutions
|
||||||
/evolve --domain testing # Only evolve instincts in testing domain
|
/evolve --generate # Also generate files under evolved/{skills,commands,agents}
|
||||||
/evolve --dry-run # Show what would be created without creating
|
|
||||||
/evolve --threshold 5 # Require 5+ related instincts to cluster
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Evolution Rules
|
## Evolution Rules
|
||||||
@@ -78,63 +76,50 @@ Example:
|
|||||||
|
|
||||||
## What to Do
|
## What to Do
|
||||||
|
|
||||||
1. Read all instincts from `~/.claude/homunculus/instincts/`
|
1. Detect current project context
|
||||||
2. Group instincts by:
|
2. Read project + global instincts (project takes precedence on ID conflicts)
|
||||||
- Domain similarity
|
3. Group instincts by trigger/domain patterns
|
||||||
- Trigger pattern overlap
|
4. Identify:
|
||||||
- Action sequence relationship
|
- Skill candidates (trigger clusters with 2+ instincts)
|
||||||
3. For each cluster of 3+ related instincts:
|
- Command candidates (high-confidence workflow instincts)
|
||||||
- Determine evolution type (command/skill/agent)
|
- Agent candidates (larger, high-confidence clusters)
|
||||||
- Generate the appropriate file
|
5. Show promotion candidates (project -> global) when applicable
|
||||||
- Save to `~/.claude/homunculus/evolved/{commands,skills,agents}/`
|
6. If `--generate` is passed, write files to:
|
||||||
4. Link evolved structure back to source instincts
|
- Project scope: `~/.claude/homunculus/projects/<project-id>/evolved/`
|
||||||
|
- Global fallback: `~/.claude/homunculus/evolved/`
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
```
|
```
|
||||||
🧬 Evolve Analysis
|
============================================================
|
||||||
==================
|
EVOLVE ANALYSIS - 12 instincts
|
||||||
|
Project: my-app (a1b2c3d4e5f6)
|
||||||
|
Project-scoped: 8 | Global: 4
|
||||||
|
============================================================
|
||||||
|
|
||||||
Found 3 clusters ready for evolution:
|
High confidence instincts (>=80%): 5
|
||||||
|
|
||||||
## Cluster 1: Database Migration Workflow
|
## SKILL CANDIDATES
|
||||||
Instincts: new-table-migration, update-schema, regenerate-types
|
1. Cluster: "adding tests"
|
||||||
Type: Command
|
Instincts: 3
|
||||||
Confidence: 85% (based on 12 observations)
|
Avg confidence: 82%
|
||||||
|
Domains: testing
|
||||||
|
Scopes: project
|
||||||
|
|
||||||
Would create: /new-table command
|
## COMMAND CANDIDATES (2)
|
||||||
Files:
|
/adding-tests
|
||||||
- ~/.claude/homunculus/evolved/commands/new-table.md
|
From: test-first-workflow [project]
|
||||||
|
Confidence: 84%
|
||||||
|
|
||||||
## Cluster 2: Functional Code Style
|
## AGENT CANDIDATES (1)
|
||||||
Instincts: prefer-functional, use-immutable, avoid-classes, pure-functions
|
adding-tests-agent
|
||||||
Type: Skill
|
Covers 3 instincts
|
||||||
Confidence: 78% (based on 8 observations)
|
Avg confidence: 82%
|
||||||
|
|
||||||
Would create: functional-patterns skill
|
|
||||||
Files:
|
|
||||||
- ~/.claude/homunculus/evolved/skills/functional-patterns.md
|
|
||||||
|
|
||||||
## Cluster 3: Debugging Process
|
|
||||||
Instincts: debug-check-logs, debug-isolate, debug-reproduce, debug-verify
|
|
||||||
Type: Agent
|
|
||||||
Confidence: 72% (based on 6 observations)
|
|
||||||
|
|
||||||
Would create: debugger agent
|
|
||||||
Files:
|
|
||||||
- ~/.claude/homunculus/evolved/agents/debugger.md
|
|
||||||
|
|
||||||
---
|
|
||||||
Run `/evolve --execute` to create these files.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
- `--execute`: Actually create the evolved structures (default is preview)
|
- `--generate`: Generate evolved files in addition to analysis output
|
||||||
- `--dry-run`: Preview without creating
|
|
||||||
- `--domain <name>`: Only evolve instincts in specified domain
|
|
||||||
- `--threshold <n>`: Minimum instincts required to form cluster (default: 3)
|
|
||||||
- `--type <command|skill|agent>`: Only create specified type
|
|
||||||
|
|
||||||
## Generated File Format
|
## Generated File Format
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: instinct-export
|
name: instinct-export
|
||||||
description: Export instincts for sharing with teammates or other projects
|
description: Export instincts from project/global scope to a file
|
||||||
command: /instinct-export
|
command: /instinct-export
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,17 +18,18 @@ Exports instincts to a shareable format. Perfect for:
|
|||||||
/instinct-export --domain testing # Export only testing instincts
|
/instinct-export --domain testing # Export only testing instincts
|
||||||
/instinct-export --min-confidence 0.7 # Only export high-confidence instincts
|
/instinct-export --min-confidence 0.7 # Only export high-confidence instincts
|
||||||
/instinct-export --output team-instincts.yaml
|
/instinct-export --output team-instincts.yaml
|
||||||
|
/instinct-export --scope project --output project-instincts.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
## What to Do
|
## What to Do
|
||||||
|
|
||||||
1. Read instincts from `~/.claude/homunculus/instincts/personal/`
|
1. Detect current project context
|
||||||
2. Filter based on flags
|
2. Load instincts by selected scope:
|
||||||
3. Strip sensitive information:
|
- `project`: current project only
|
||||||
- Remove session IDs
|
- `global`: global only
|
||||||
- Remove file paths (keep only patterns)
|
- `all`: project + global merged (default)
|
||||||
- Remove timestamps older than "last week"
|
3. Apply filters (`--domain`, `--min-confidence`)
|
||||||
4. Generate export file
|
4. Write YAML-style export to file (or stdout if no output path provided)
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
@@ -40,52 +41,26 @@ Creates a YAML file:
|
|||||||
# Source: personal
|
# Source: personal
|
||||||
# Count: 12 instincts
|
# Count: 12 instincts
|
||||||
|
|
||||||
version: "2.0"
|
---
|
||||||
exported_by: "continuous-learning-v2"
|
id: prefer-functional-style
|
||||||
export_date: "2025-01-22T10:30:00Z"
|
trigger: "when writing new functions"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: code-style
|
||||||
|
source: session-observation
|
||||||
|
scope: project
|
||||||
|
project_id: a1b2c3d4e5f6
|
||||||
|
project_name: my-app
|
||||||
|
---
|
||||||
|
|
||||||
instincts:
|
# Prefer Functional Style
|
||||||
- id: prefer-functional-style
|
|
||||||
trigger: "when writing new functions"
|
|
||||||
action: "Use functional patterns over classes"
|
|
||||||
confidence: 0.8
|
|
||||||
domain: code-style
|
|
||||||
observations: 8
|
|
||||||
|
|
||||||
- id: test-first-workflow
|
## Action
|
||||||
trigger: "when adding new functionality"
|
Use functional patterns over classes.
|
||||||
action: "Write test first, then implementation"
|
|
||||||
confidence: 0.9
|
|
||||||
domain: testing
|
|
||||||
observations: 12
|
|
||||||
|
|
||||||
- id: grep-before-edit
|
|
||||||
trigger: "when modifying code"
|
|
||||||
action: "Search with Grep, confirm with Read, then Edit"
|
|
||||||
confidence: 0.7
|
|
||||||
domain: workflow
|
|
||||||
observations: 6
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Privacy Considerations
|
|
||||||
|
|
||||||
Exports include:
|
|
||||||
- ✅ Trigger patterns
|
|
||||||
- ✅ Actions
|
|
||||||
- ✅ Confidence scores
|
|
||||||
- ✅ Domains
|
|
||||||
- ✅ Observation counts
|
|
||||||
|
|
||||||
Exports do NOT include:
|
|
||||||
- ❌ Actual code snippets
|
|
||||||
- ❌ File paths
|
|
||||||
- ❌ Session transcripts
|
|
||||||
- ❌ Personal identifiers
|
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
- `--domain <name>`: Export only specified domain
|
- `--domain <name>`: Export only specified domain
|
||||||
- `--min-confidence <n>`: Minimum confidence threshold (default: 0.3)
|
- `--min-confidence <n>`: Minimum confidence threshold
|
||||||
- `--output <file>`: Output file path (default: instincts-export-YYYYMMDD.yaml)
|
- `--output <file>`: Output file path (prints to stdout when omitted)
|
||||||
- `--format <yaml|json|md>`: Output format (default: yaml)
|
- `--scope <project|global|all>`: Export scope (default: `all`)
|
||||||
- `--include-evidence`: Include evidence text (default: excluded)
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: instinct-import
|
name: instinct-import
|
||||||
description: Import instincts from teammates, Skill Creator, or other sources
|
description: Import instincts from file or URL into project/global scope
|
||||||
command: true
|
command: true
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ command: true
|
|||||||
Run the instinct CLI using the plugin root path:
|
Run the instinct CLI using the plugin root path:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" import <file-or-url> [--dry-run] [--force] [--min-confidence 0.7]
|
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" import <file-or-url> [--dry-run] [--force] [--min-confidence 0.7] [--scope project|global]
|
||||||
```
|
```
|
||||||
|
|
||||||
Or if `CLAUDE_PLUGIN_ROOT` is not set (manual installation):
|
Or if `CLAUDE_PLUGIN_ROOT` is not set (manual installation):
|
||||||
@@ -20,18 +20,15 @@ Or if `CLAUDE_PLUGIN_ROOT` is not set (manual installation):
|
|||||||
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py import <file-or-url>
|
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py import <file-or-url>
|
||||||
```
|
```
|
||||||
|
|
||||||
Import instincts from:
|
Import instincts from local file paths or HTTP(S) URLs.
|
||||||
- Teammates' exports
|
|
||||||
- Skill Creator (repo analysis)
|
|
||||||
- Community collections
|
|
||||||
- Previous machine backups
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
/instinct-import team-instincts.yaml
|
/instinct-import team-instincts.yaml
|
||||||
/instinct-import https://github.com/org/repo/instincts.yaml
|
/instinct-import https://github.com/org/repo/instincts.yaml
|
||||||
/instinct-import --from-skill-creator acme/webapp
|
/instinct-import team-instincts.yaml --dry-run
|
||||||
|
/instinct-import team-instincts.yaml --scope global --force
|
||||||
```
|
```
|
||||||
|
|
||||||
## What to Do
|
## What to Do
|
||||||
@@ -40,7 +37,9 @@ Import instincts from:
|
|||||||
2. Parse and validate the format
|
2. Parse and validate the format
|
||||||
3. Check for duplicates with existing instincts
|
3. Check for duplicates with existing instincts
|
||||||
4. Merge or add new instincts
|
4. Merge or add new instincts
|
||||||
5. Save to `~/.claude/homunculus/instincts/inherited/`
|
5. Save to inherited instincts directory:
|
||||||
|
- Project scope: `~/.claude/homunculus/projects/<project-id>/instincts/inherited/`
|
||||||
|
- Global scope: `~/.claude/homunculus/instincts/inherited/`
|
||||||
|
|
||||||
## Import Process
|
## Import Process
|
||||||
|
|
||||||
@@ -71,60 +70,33 @@ Already have similar instincts:
|
|||||||
Import: 0.9 confidence
|
Import: 0.9 confidence
|
||||||
→ Update to import (higher confidence)
|
→ Update to import (higher confidence)
|
||||||
|
|
||||||
## Conflicting Instincts (1)
|
Import 8 new, update 1?
|
||||||
These contradict local instincts:
|
|
||||||
❌ use-classes-for-services
|
|
||||||
Conflicts with: avoid-classes
|
|
||||||
→ Skip (requires manual resolution)
|
|
||||||
|
|
||||||
---
|
|
||||||
Import 8 new, update 1, skip 3?
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Merge Strategies
|
## Merge Behavior
|
||||||
|
|
||||||
### For Duplicates
|
When importing an instinct with an existing ID:
|
||||||
When importing an instinct that matches an existing one:
|
- Higher-confidence import becomes an update candidate
|
||||||
- **Higher confidence wins**: Keep the one with higher confidence
|
- Equal/lower-confidence import is skipped
|
||||||
- **Merge evidence**: Combine observation counts
|
- User confirms unless `--force` is used
|
||||||
- **Update timestamp**: Mark as recently validated
|
|
||||||
|
|
||||||
### For Conflicts
|
|
||||||
When importing an instinct that contradicts an existing one:
|
|
||||||
- **Skip by default**: Don't import conflicting instincts
|
|
||||||
- **Flag for review**: Mark both as needing attention
|
|
||||||
- **Manual resolution**: User decides which to keep
|
|
||||||
|
|
||||||
## Source Tracking
|
## Source Tracking
|
||||||
|
|
||||||
Imported instincts are marked with:
|
Imported instincts are marked with:
|
||||||
```yaml
|
```yaml
|
||||||
source: "inherited"
|
source: inherited
|
||||||
|
scope: project
|
||||||
imported_from: "team-instincts.yaml"
|
imported_from: "team-instincts.yaml"
|
||||||
imported_at: "2025-01-22T10:30:00Z"
|
project_id: "a1b2c3d4e5f6"
|
||||||
original_source: "session-observation" # or "repo-analysis"
|
project_name: "my-project"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skill Creator Integration
|
|
||||||
|
|
||||||
When importing from Skill Creator:
|
|
||||||
|
|
||||||
```
|
|
||||||
/instinct-import --from-skill-creator acme/webapp
|
|
||||||
```
|
|
||||||
|
|
||||||
This fetches instincts generated from repo analysis:
|
|
||||||
- Source: `repo-analysis`
|
|
||||||
- Higher initial confidence (0.7+)
|
|
||||||
- Linked to source repository
|
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
- `--dry-run`: Preview without importing
|
- `--dry-run`: Preview without importing
|
||||||
- `--force`: Import even if conflicts exist
|
- `--force`: Skip confirmation prompt
|
||||||
- `--merge-strategy <higher|local|import>`: How to handle duplicates
|
|
||||||
- `--from-skill-creator <owner/repo>`: Import from Skill Creator analysis
|
|
||||||
- `--min-confidence <n>`: Only import instincts above threshold
|
- `--min-confidence <n>`: Only import instincts above threshold
|
||||||
|
- `--scope <project|global>`: Select target scope (default: `project`)
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
@@ -134,7 +106,7 @@ After import:
|
|||||||
|
|
||||||
Added: 8 instincts
|
Added: 8 instincts
|
||||||
Updated: 1 instinct
|
Updated: 1 instinct
|
||||||
Skipped: 3 instincts (2 duplicates, 1 conflict)
|
Skipped: 3 instincts (equal/higher confidence already exists)
|
||||||
|
|
||||||
New instincts saved to: ~/.claude/homunculus/instincts/inherited/
|
New instincts saved to: ~/.claude/homunculus/instincts/inherited/
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: instinct-status
|
name: instinct-status
|
||||||
description: Show all learned instincts with their confidence levels
|
description: Show learned instincts (project + global) with confidence
|
||||||
command: true
|
command: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Instinct Status Command
|
# Instinct Status Command
|
||||||
|
|
||||||
Shows all learned instincts with their confidence scores, grouped by domain.
|
Shows learned instincts for the current project plus global instincts, grouped by domain.
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
@@ -26,61 +26,34 @@ python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status
|
|||||||
|
|
||||||
```
|
```
|
||||||
/instinct-status
|
/instinct-status
|
||||||
/instinct-status --domain code-style
|
|
||||||
/instinct-status --low-confidence
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## What to Do
|
## What to Do
|
||||||
|
|
||||||
1. Read all instinct files from `~/.claude/homunculus/instincts/personal/`
|
1. Detect current project context (git remote/path hash)
|
||||||
2. Read inherited instincts from `~/.claude/homunculus/instincts/inherited/`
|
2. Read project instincts from `~/.claude/homunculus/projects/<project-id>/instincts/`
|
||||||
3. Display them grouped by domain with confidence bars
|
3. Read global instincts from `~/.claude/homunculus/instincts/`
|
||||||
|
4. Merge with precedence rules (project overrides global when IDs collide)
|
||||||
|
5. Display grouped by domain with confidence bars and observation stats
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
```
|
```
|
||||||
📊 Instinct Status
|
============================================================
|
||||||
==================
|
INSTINCT STATUS - 12 total
|
||||||
|
============================================================
|
||||||
|
|
||||||
## Code Style (4 instincts)
|
Project: my-app (a1b2c3d4e5f6)
|
||||||
|
Project instincts: 8
|
||||||
|
Global instincts: 4
|
||||||
|
|
||||||
### prefer-functional-style
|
## PROJECT-SCOPED (my-app)
|
||||||
Trigger: when writing new functions
|
### WORKFLOW (3)
|
||||||
Action: Use functional patterns over classes
|
███████░░░ 70% grep-before-edit [project]
|
||||||
Confidence: ████████░░ 80%
|
trigger: when modifying code
|
||||||
Source: session-observation | Last updated: 2025-01-22
|
|
||||||
|
|
||||||
### use-path-aliases
|
## GLOBAL (apply to all projects)
|
||||||
Trigger: when importing modules
|
### SECURITY (2)
|
||||||
Action: Use @/ path aliases instead of relative imports
|
█████████░ 85% validate-user-input [global]
|
||||||
Confidence: ██████░░░░ 60%
|
trigger: when handling user input
|
||||||
Source: repo-analysis (github.com/acme/webapp)
|
|
||||||
|
|
||||||
## Testing (2 instincts)
|
|
||||||
|
|
||||||
### test-first-workflow
|
|
||||||
Trigger: when adding new functionality
|
|
||||||
Action: Write test first, then implementation
|
|
||||||
Confidence: █████████░ 90%
|
|
||||||
Source: session-observation
|
|
||||||
|
|
||||||
## Workflow (3 instincts)
|
|
||||||
|
|
||||||
### grep-before-edit
|
|
||||||
Trigger: when modifying code
|
|
||||||
Action: Search with Grep, confirm with Read, then Edit
|
|
||||||
Confidence: ███████░░░ 70%
|
|
||||||
Source: session-observation
|
|
||||||
|
|
||||||
---
|
|
||||||
Total: 9 instincts (4 personal, 5 inherited)
|
|
||||||
Observer: Running (last analysis: 5 min ago)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Flags
|
|
||||||
|
|
||||||
- `--domain <name>`: Filter by domain (code-style, testing, git, etc.)
|
|
||||||
- `--low-confidence`: Show only instincts with confidence < 0.5
|
|
||||||
- `--high-confidence`: Show only instincts with confidence >= 0.7
|
|
||||||
- `--source <type>`: Filter by source (session-observation, repo-analysis, inherited)
|
|
||||||
- `--json`: Output as JSON for programmatic use
|
|
||||||
|
|||||||
40
commands/projects.md
Normal file
40
commands/projects.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: projects
|
||||||
|
description: List known projects and their instinct statistics
|
||||||
|
command: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Projects Command
|
||||||
|
|
||||||
|
List project registry entries and per-project instinct/observation counts for continuous-learning-v2.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Run the instinct CLI using the plugin root path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" projects
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if `CLAUDE_PLUGIN_ROOT` is not set (manual installation):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py projects
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/projects
|
||||||
|
```
|
||||||
|
|
||||||
|
## What to Do
|
||||||
|
|
||||||
|
1. Read `~/.claude/homunculus/projects.json`
|
||||||
|
2. For each project, display:
|
||||||
|
- Project name, id, root, remote
|
||||||
|
- Personal and inherited instinct counts
|
||||||
|
- Observation event count
|
||||||
|
- Last seen timestamp
|
||||||
|
3. Also display global instinct totals
|
||||||
|
|
||||||
42
commands/promote.md
Normal file
42
commands/promote.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: promote
|
||||||
|
description: Promote project-scoped instincts to global scope
|
||||||
|
command: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Promote Command
|
||||||
|
|
||||||
|
Promote instincts from project scope to global scope in continuous-learning-v2.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Run the instinct CLI using the plugin root path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" promote [instinct-id] [--force] [--dry-run]
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if `CLAUDE_PLUGIN_ROOT` is not set (manual installation):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py promote [instinct-id] [--force] [--dry-run]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/promote # Auto-detect promotion candidates
|
||||||
|
/promote --dry-run # Preview auto-promotion candidates
|
||||||
|
/promote --force # Promote all qualified candidates without prompt
|
||||||
|
/promote grep-before-edit # Promote one specific instinct from current project
|
||||||
|
```
|
||||||
|
|
||||||
|
## What to Do
|
||||||
|
|
||||||
|
1. Detect current project
|
||||||
|
2. If `instinct-id` is provided, promote only that instinct (if present in current project)
|
||||||
|
3. Otherwise, find cross-project candidates that:
|
||||||
|
- Appear in at least 2 projects
|
||||||
|
- Meet confidence threshold
|
||||||
|
4. Write promoted instincts to `~/.claude/homunculus/instincts/personal/` with `scope: global`
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pre-write-doc-warn.js\""
|
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.(md|txt)$/.test(p)&&!/(README|CLAUDE|AGENTS|CONTRIBUTING|CHANGELOG|LICENSE|SKILL)\\.md$/i.test(p)&&!/\\.claude[\\/\\\\]plans[\\/\\\\]/.test(p)&&!/(^|[\\/\\\\])(docs|skills)[\\/\\\\]/.test(p)){console.error('[Hook] WARNING: Non-standard documentation file detected');console.error('[Hook] File: '+p);console.error('[Hook] Consider consolidating into README.md or docs/ directory')}}catch{}console.log(d)})\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Doc file warning: warn about non-standard documentation files (exit code 0; warns only)"
|
"description": "Doc file warning: warn about non-standard documentation files (exit code 0; warns only)"
|
||||||
@@ -51,6 +51,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Suggest manual compaction at logical intervals"
|
"description": "Suggest manual compaction at logical intervals"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh",
|
||||||
|
"async": true,
|
||||||
|
"timeout": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Capture tool use observations for continuous learning"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"PreCompact": [
|
"PreCompact": [
|
||||||
@@ -129,6 +141,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Warn about console.log statements after edits"
|
"description": "Warn about console.log statements after edits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh",
|
||||||
|
"async": true,
|
||||||
|
"timeout": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Capture tool use results for continuous learning"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Stop": [
|
"Stop": [
|
||||||
|
|||||||
@@ -66,6 +66,14 @@
|
|||||||
"url": "https://mcp.clickhouse.cloud/mcp",
|
"url": "https://mcp.clickhouse.cloud/mcp",
|
||||||
"description": "ClickHouse analytics queries"
|
"description": "ClickHouse analytics queries"
|
||||||
},
|
},
|
||||||
|
"exa-web-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "exa-mcp-server"],
|
||||||
|
"env": {
|
||||||
|
"EXA_API_KEY": "YOUR_EXA_API_KEY_HERE"
|
||||||
|
},
|
||||||
|
"description": "Web search, research, and data ingestion via Exa API — recommended for research-first development workflow"
|
||||||
|
},
|
||||||
"context7": {
|
"context7": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "@context7/mcp-server"],
|
"args": ["-y", "@context7/mcp-server"],
|
||||||
|
|||||||
@@ -2,12 +2,20 @@
|
|||||||
|
|
||||||
> This file extends [common/git-workflow.md](./git-workflow.md) with the full feature development process that happens before git operations.
|
> This file extends [common/git-workflow.md](./git-workflow.md) with the full feature development process that happens before git operations.
|
||||||
|
|
||||||
The Feature Implementation Workflow describes the development pipeline: planning, TDD, code review, and then committing to git.
|
The Feature Implementation Workflow describes the development pipeline: research, planning, TDD, code review, and then committing to git.
|
||||||
|
|
||||||
## Feature Implementation Workflow
|
## Feature Implementation Workflow
|
||||||
|
|
||||||
|
0. **Research & Reuse** _(mandatory before any new implementation)_
|
||||||
|
- **GitHub code search first:** Run `gh search repos` and `gh search code` to find existing implementations, templates, and patterns before writing anything new.
|
||||||
|
- **Exa MCP for research:** Use `exa-web-search` MCP during the planning phase for broader research, data ingestion, and discovering prior art.
|
||||||
|
- **Check package registries:** Search npm, PyPI, crates.io, and other registries before writing utility code. Prefer battle-tested libraries over hand-rolled solutions.
|
||||||
|
- **Search for adaptable implementations:** Look for open-source projects that solve 80%+ of the problem and can be forked, ported, or wrapped.
|
||||||
|
- Prefer adopting or porting a proven approach over writing net-new code when it meets the requirement.
|
||||||
|
|
||||||
1. **Plan First**
|
1. **Plan First**
|
||||||
- Use **planner** agent to create implementation plan
|
- Use **planner** agent to create implementation plan
|
||||||
|
- Generate planning docs before coding: PRD, architecture, system_design, tech_doc, task_list
|
||||||
- Identify dependencies and risks
|
- Identify dependencies and risks
|
||||||
- Break down into phases
|
- Break down into phases
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,23 @@ mkdir -p $TARGET/skills $TARGET/rules
|
|||||||
|
|
||||||
## Step 2: Select & Install Skills
|
## Step 2: Select & Install Skills
|
||||||
|
|
||||||
### 2a: Choose Skill Categories
|
### 2a: Choose Scope (Core vs Niche)
|
||||||
|
|
||||||
|
Default to **Core (recommended for new users)** — copy `.agents/skills/*` plus `skills/search-first/` for research-first workflows. This bundle covers engineering, evals, verification, security, strategic compaction, frontend design, and Anthropic cross-functional skills (article-writing, content-engine, market-research, frontend-slides).
|
||||||
|
|
||||||
|
Use `AskUserQuestion` (single select):
|
||||||
|
```
|
||||||
|
Question: "Install core skills only, or include niche/framework packs?"
|
||||||
|
Options:
|
||||||
|
- "Core only (recommended)" — "tdd, e2e, evals, verification, research-first, security, frontend patterns, compacting, cross-functional Anthropic skills"
|
||||||
|
- "Core + selected niche" — "Add framework/domain-specific skills after core"
|
||||||
|
- "Niche only" — "Skip core, install specific framework/domain skills"
|
||||||
|
Default: Core only
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user chooses niche or core + niche, continue to category selection below and only include those niche skills they pick.
|
||||||
|
|
||||||
|
### 2b: Choose Skill Categories
|
||||||
|
|
||||||
There are 27 skills organized into 4 categories. Use `AskUserQuestion` with `multiSelect: true`:
|
There are 27 skills organized into 4 categories. Use `AskUserQuestion` with `multiSelect: true`:
|
||||||
|
|
||||||
@@ -77,7 +93,7 @@ Options:
|
|||||||
- "All skills" — "Install every available skill"
|
- "All skills" — "Install every available skill"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2b: Confirm Individual Skills
|
### 2c: Confirm Individual Skills
|
||||||
|
|
||||||
For each selected category, print the full list of skills below and ask the user to confirm or deselect specific ones. If the list exceeds 4 items, print the list as text and use `AskUserQuestion` with an "Install all listed" option plus "Other" for the user to paste specific names.
|
For each selected category, print the full list of skills below and ask the user to confirm or deselect specific ones. If the list exceeds 4 items, print the list as text and use `AskUserQuestion` with an "Install all listed" option plus "Other" for the user to paste specific names.
|
||||||
|
|
||||||
@@ -140,7 +156,7 @@ For each selected category, print the full list of skills below and ask the user
|
|||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `project-guidelines-example` | Template for creating project-specific skills |
|
| `project-guidelines-example` | Template for creating project-specific skills |
|
||||||
|
|
||||||
### 2c: Execute Installation
|
### 2d: Execute Installation
|
||||||
|
|
||||||
For each selected skill, copy the entire skill directory:
|
For each selected skill, copy the entire skill directory:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
---
|
---
|
||||||
name: continuous-learning-v2
|
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.
|
description: Instinct-based learning system that observes sessions via hooks, creates atomic instincts with confidence scoring, and evolves them into skills/commands/agents. v2.1 adds project-scoped instincts to prevent cross-project contamination.
|
||||||
origin: ECC
|
origin: ECC
|
||||||
version: 2.0.0
|
version: 2.1.0
|
||||||
---
|
---
|
||||||
|
|
||||||
# Continuous Learning v2 - Instinct-Based Architecture
|
# Continuous Learning v2.1 - 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.
|
An advanced learning system that turns your Claude Code sessions into reusable knowledge through atomic "instincts" - small learned behaviors with confidence scoring.
|
||||||
|
|
||||||
Inspired in part by the Homunculus work from [humanplane](https://github.com/humanplane).
|
**v2.1** adds **project-scoped instincts** — React patterns stay in your React project, Python conventions stay in your Python project, and universal patterns (like "always validate input") are shared globally.
|
||||||
|
|
||||||
## When to Activate
|
## When to Activate
|
||||||
|
|
||||||
@@ -18,8 +19,21 @@ Inspired in part by the Homunculus work from [humanplane](https://github.com/hum
|
|||||||
- Tuning confidence thresholds for learned behaviors
|
- Tuning confidence thresholds for learned behaviors
|
||||||
- Reviewing, exporting, or importing instinct libraries
|
- Reviewing, exporting, or importing instinct libraries
|
||||||
- Evolving instincts into full skills, commands, or agents
|
- Evolving instincts into full skills, commands, or agents
|
||||||
|
- Managing project-scoped vs global instincts
|
||||||
|
- Promoting instincts from project to global scope
|
||||||
|
|
||||||
## What's New in v2
|
## What's New in v2.1
|
||||||
|
|
||||||
|
| Feature | v2.0 | v2.1 |
|
||||||
|
|---------|------|------|
|
||||||
|
| Storage | Global (~/.claude/homunculus/) | Project-scoped (projects/<hash>/) |
|
||||||
|
| Scope | All instincts apply everywhere | Project-scoped + global |
|
||||||
|
| Detection | None | git remote URL / repo path |
|
||||||
|
| Promotion | N/A | Project → global when seen in 2+ projects |
|
||||||
|
| Commands | 4 (status/evolve/export/import) | 6 (+promote/projects) |
|
||||||
|
| Cross-project | Contamination risk | Isolated by default |
|
||||||
|
|
||||||
|
## What's New in v2 (vs v1)
|
||||||
|
|
||||||
| Feature | v1 | v2 |
|
| Feature | v1 | v2 |
|
||||||
|---------|----|----|
|
|---------|----|----|
|
||||||
@@ -27,7 +41,7 @@ Inspired in part by the Homunculus work from [humanplane](https://github.com/hum
|
|||||||
| Analysis | Main context | Background agent (Haiku) |
|
| Analysis | Main context | Background agent (Haiku) |
|
||||||
| Granularity | Full skills | Atomic "instincts" |
|
| Granularity | Full skills | Atomic "instincts" |
|
||||||
| Confidence | None | 0.3-0.9 weighted |
|
| Confidence | None | 0.3-0.9 weighted |
|
||||||
| Evolution | Direct to skill | Instincts → cluster → skill/command/agent |
|
| Evolution | Direct to skill | Instincts -> cluster -> skill/command/agent |
|
||||||
| Sharing | None | Export/import instincts |
|
| Sharing | None | Export/import instincts |
|
||||||
|
|
||||||
## The Instinct Model
|
## The Instinct Model
|
||||||
@@ -41,6 +55,9 @@ trigger: "when writing new functions"
|
|||||||
confidence: 0.7
|
confidence: 0.7
|
||||||
domain: "code-style"
|
domain: "code-style"
|
||||||
source: "session-observation"
|
source: "session-observation"
|
||||||
|
scope: project
|
||||||
|
project_id: "a1b2c3d4e5f6"
|
||||||
|
project_name: "my-react-app"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Prefer Functional Style
|
# Prefer Functional Style
|
||||||
@@ -54,51 +71,69 @@ Use functional patterns over classes when appropriate.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Properties:**
|
**Properties:**
|
||||||
- **Atomic** — one trigger, one action
|
- **Atomic** -- one trigger, one action
|
||||||
- **Confidence-weighted** — 0.3 = tentative, 0.9 = near certain
|
- **Confidence-weighted** -- 0.3 = tentative, 0.9 = near certain
|
||||||
- **Domain-tagged** — code-style, testing, git, debugging, workflow, etc.
|
- **Domain-tagged** -- code-style, testing, git, debugging, workflow, etc.
|
||||||
- **Evidence-backed** — tracks what observations created it
|
- **Evidence-backed** -- tracks what observations created it
|
||||||
|
- **Scope-aware** -- `project` (default) or `global`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
```
|
```
|
||||||
Session Activity
|
Session Activity (in a git repo)
|
||||||
│
|
|
|
||||||
│ Hooks capture prompts + tool use (100% reliable)
|
| Hooks capture prompts + tool use (100% reliable)
|
||||||
▼
|
| + detect project context (git remote / repo path)
|
||||||
┌─────────────────────────────────────────┐
|
v
|
||||||
│ observations.jsonl │
|
+---------------------------------------------+
|
||||||
│ (prompts, tool calls, outcomes) │
|
| projects/<project-hash>/observations.jsonl |
|
||||||
└─────────────────────────────────────────┘
|
| (prompts, tool calls, outcomes, project) |
|
||||||
│
|
+---------------------------------------------+
|
||||||
│ Observer agent reads (background, Haiku)
|
|
|
||||||
▼
|
| Observer agent reads (background, Haiku)
|
||||||
┌─────────────────────────────────────────┐
|
v
|
||||||
│ PATTERN DETECTION │
|
+---------------------------------------------+
|
||||||
│ • User corrections → instinct │
|
| PATTERN DETECTION |
|
||||||
│ • Error resolutions → instinct │
|
| * User corrections -> instinct |
|
||||||
│ • Repeated workflows → instinct │
|
| * Error resolutions -> instinct |
|
||||||
└─────────────────────────────────────────┘
|
| * Repeated workflows -> instinct |
|
||||||
│
|
| * Scope decision: project or global? |
|
||||||
│ Creates/updates
|
+---------------------------------------------+
|
||||||
▼
|
|
|
||||||
┌─────────────────────────────────────────┐
|
| Creates/updates
|
||||||
│ instincts/personal/ │
|
v
|
||||||
│ • prefer-functional.md (0.7) │
|
+---------------------------------------------+
|
||||||
│ • always-test-first.md (0.9) │
|
| projects/<project-hash>/instincts/personal/ |
|
||||||
│ • use-zod-validation.md (0.6) │
|
| * prefer-functional.yaml (0.7) [project] |
|
||||||
└─────────────────────────────────────────┘
|
| * use-react-hooks.yaml (0.9) [project] |
|
||||||
│
|
+---------------------------------------------+
|
||||||
│ /evolve clusters
|
| instincts/personal/ (GLOBAL) |
|
||||||
▼
|
| * always-validate-input.yaml (0.85) [global]|
|
||||||
┌─────────────────────────────────────────┐
|
| * grep-before-edit.yaml (0.6) [global] |
|
||||||
│ evolved/ │
|
+---------------------------------------------+
|
||||||
│ • commands/new-feature.md │
|
|
|
||||||
│ • skills/testing-workflow.md │
|
| /evolve clusters + /promote
|
||||||
│ • agents/refactor-specialist.md │
|
v
|
||||||
└─────────────────────────────────────────┘
|
+---------------------------------------------+
|
||||||
|
| projects/<hash>/evolved/ (project-scoped) |
|
||||||
|
| evolved/ (global) |
|
||||||
|
| * commands/new-feature.md |
|
||||||
|
| * skills/testing-workflow.md |
|
||||||
|
| * agents/refactor-specialist.md |
|
||||||
|
+---------------------------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Project Detection
|
||||||
|
|
||||||
|
The system automatically detects your current project:
|
||||||
|
|
||||||
|
1. **`CLAUDE_PROJECT_DIR` env var** (highest priority)
|
||||||
|
2. **`git remote get-url origin`** -- hashed to create a portable project ID (same repo on different machines gets the same ID)
|
||||||
|
3. **`git rev-parse --show-toplevel`** -- fallback using repo path (machine-specific)
|
||||||
|
4. **Global fallback** -- if no project is detected, instincts go to global scope
|
||||||
|
|
||||||
|
Each project gets a 12-character hash ID (e.g., `a1b2c3d4e5f6`). A registry file at `~/.claude/homunculus/projects.json` maps IDs to human-readable names.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Enable Observation Hooks
|
### 1. Enable Observation Hooks
|
||||||
@@ -114,14 +149,14 @@ Add to your `~/.claude/settings.json`.
|
|||||||
"matcher": "*",
|
"matcher": "*",
|
||||||
"hooks": [{
|
"hooks": [{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh pre"
|
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
}]
|
}]
|
||||||
}],
|
}],
|
||||||
"PostToolUse": [{
|
"PostToolUse": [{
|
||||||
"matcher": "*",
|
"matcher": "*",
|
||||||
"hooks": [{
|
"hooks": [{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh post"
|
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -137,14 +172,14 @@ Add to your `~/.claude/settings.json`.
|
|||||||
"matcher": "*",
|
"matcher": "*",
|
||||||
"hooks": [{
|
"hooks": [{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh pre"
|
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
}]
|
}]
|
||||||
}],
|
}],
|
||||||
"PostToolUse": [{
|
"PostToolUse": [{
|
||||||
"matcher": "*",
|
"matcher": "*",
|
||||||
"hooks": [{
|
"hooks": [{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh post"
|
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh"
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -153,92 +188,124 @@ Add to your `~/.claude/settings.json`.
|
|||||||
|
|
||||||
### 2. Initialize Directory Structure
|
### 2. Initialize Directory Structure
|
||||||
|
|
||||||
The Python CLI will create these automatically, but you can also create them manually:
|
The system creates directories automatically on first use, but you can also create them manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands}}
|
# Global directories
|
||||||
touch ~/.claude/homunculus/observations.jsonl
|
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands},projects}
|
||||||
|
|
||||||
|
# Project directories are auto-created when the hook first runs in a git repo
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Use the Instinct Commands
|
### 3. Use the Instinct Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/instinct-status # Show learned instincts with confidence scores
|
/instinct-status # Show learned instincts (project + global)
|
||||||
/evolve # Cluster related instincts into skills/commands
|
/evolve # Cluster related instincts into skills/commands
|
||||||
/instinct-export # Export instincts for sharing
|
/instinct-export # Export instincts to file
|
||||||
/instinct-import # Import instincts from others
|
/instinct-import # Import instincts from others
|
||||||
|
/promote # Promote project instincts to global scope
|
||||||
|
/projects # List all known projects and their instinct counts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `/instinct-status` | Show all learned instincts with confidence |
|
| `/instinct-status` | Show all instincts (project-scoped + global) with confidence |
|
||||||
| `/evolve` | Cluster related instincts into skills/commands |
|
| `/evolve` | Cluster related instincts into skills/commands, suggest promotions |
|
||||||
| `/instinct-export` | Export instincts for sharing |
|
| `/instinct-export` | Export instincts (filterable by scope/domain) |
|
||||||
| `/instinct-import <file>` | Import instincts from others |
|
| `/instinct-import <file>` | Import instincts with scope control |
|
||||||
|
| `/promote [id]` | Promote project instincts to global scope |
|
||||||
|
| `/projects` | List all known projects and their instinct counts |
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit `config.json`:
|
Edit `config.json` to control the background observer:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "2.0",
|
"version": "2.1",
|
||||||
"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": {
|
"observer": {
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"model": "haiku",
|
|
||||||
"run_interval_minutes": 5,
|
"run_interval_minutes": 5,
|
||||||
"patterns_to_detect": [
|
"min_observations_to_analyze": 20
|
||||||
"user_corrections",
|
|
||||||
"error_resolutions",
|
|
||||||
"repeated_workflows",
|
|
||||||
"tool_preferences"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"evolution": {
|
|
||||||
"cluster_threshold": 3,
|
|
||||||
"evolved_path": "~/.claude/homunculus/evolved/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
|-----|---------|-------------|
|
||||||
|
| `observer.enabled` | `false` | Enable the background observer agent |
|
||||||
|
| `observer.run_interval_minutes` | `5` | How often the observer analyzes observations |
|
||||||
|
| `observer.min_observations_to_analyze` | `20` | Minimum observations before analysis runs |
|
||||||
|
|
||||||
|
Other behavior (observation capture, instinct thresholds, project scoping, promotion criteria) is configured via code defaults in `instinct-cli.py` and `observe.sh`.
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.claude/homunculus/
|
~/.claude/homunculus/
|
||||||
├── identity.json # Your profile, technical level
|
+-- identity.json # Your profile, technical level
|
||||||
├── observations.jsonl # Current session observations
|
+-- projects.json # Registry: project hash -> name/path/remote
|
||||||
├── observations.archive/ # Processed observations
|
+-- observations.jsonl # Global observations (fallback)
|
||||||
├── instincts/
|
+-- instincts/
|
||||||
│ ├── personal/ # Auto-learned instincts
|
| +-- personal/ # Global auto-learned instincts
|
||||||
│ └── inherited/ # Imported from others
|
| +-- inherited/ # Global imported instincts
|
||||||
└── evolved/
|
+-- evolved/
|
||||||
├── agents/ # Generated specialist agents
|
| +-- agents/ # Global generated agents
|
||||||
├── skills/ # Generated skills
|
| +-- skills/ # Global generated skills
|
||||||
└── commands/ # Generated commands
|
| +-- commands/ # Global generated commands
|
||||||
|
+-- projects/
|
||||||
|
+-- a1b2c3d4e5f6/ # Project hash (from git remote URL)
|
||||||
|
| +-- observations.jsonl
|
||||||
|
| +-- observations.archive/
|
||||||
|
| +-- instincts/
|
||||||
|
| | +-- personal/ # Project-specific auto-learned
|
||||||
|
| | +-- inherited/ # Project-specific imported
|
||||||
|
| +-- evolved/
|
||||||
|
| +-- skills/
|
||||||
|
| +-- commands/
|
||||||
|
| +-- agents/
|
||||||
|
+-- f6e5d4c3b2a1/ # Another project
|
||||||
|
+-- ...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integration with Skill Creator
|
## Scope Decision Guide
|
||||||
|
|
||||||
When you use the [Skill Creator GitHub App](https://skill-creator.app), it now generates **both**:
|
| Pattern Type | Scope | Examples |
|
||||||
- Traditional SKILL.md files (for backward compatibility)
|
|-------------|-------|---------|
|
||||||
- Instinct collections (for v2 learning system)
|
| Language/framework conventions | **project** | "Use React hooks", "Follow Django REST patterns" |
|
||||||
|
| File structure preferences | **project** | "Tests in __tests__/", "Components in src/components/" |
|
||||||
|
| Code style | **project** | "Use functional style", "Prefer dataclasses" |
|
||||||
|
| Error handling strategies | **project** | "Use Result type for errors" |
|
||||||
|
| Security practices | **global** | "Validate user input", "Sanitize SQL" |
|
||||||
|
| General best practices | **global** | "Write tests first", "Always handle errors" |
|
||||||
|
| Tool workflow preferences | **global** | "Grep before Edit", "Read before Write" |
|
||||||
|
| Git practices | **global** | "Conventional commits", "Small focused commits" |
|
||||||
|
|
||||||
Instincts from repo analysis have `source: "repo-analysis"` and include the source repository URL.
|
## Instinct Promotion (Project -> Global)
|
||||||
|
|
||||||
|
When the same instinct appears in multiple projects with high confidence, it's a candidate for promotion to global scope.
|
||||||
|
|
||||||
|
**Auto-promotion criteria:**
|
||||||
|
- Same instinct ID in 2+ projects
|
||||||
|
- Average confidence >= 0.8
|
||||||
|
|
||||||
|
**How to promote:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Promote a specific instinct
|
||||||
|
python3 instinct-cli.py promote prefer-explicit-errors
|
||||||
|
|
||||||
|
# Auto-promote all qualifying instincts
|
||||||
|
python3 instinct-cli.py promote
|
||||||
|
|
||||||
|
# Preview without changes
|
||||||
|
python3 instinct-cli.py promote --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
The `/evolve` command also suggests promotion candidates.
|
||||||
|
|
||||||
## Confidence Scoring
|
## Confidence Scoring
|
||||||
|
|
||||||
@@ -263,7 +330,7 @@ Confidence evolves over time:
|
|||||||
|
|
||||||
## Why Hooks vs Skills for Observation?
|
## 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."
|
> "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:
|
Hooks fire **100% of the time**, deterministically. This means:
|
||||||
- Every tool call is observed
|
- Every tool call is observed
|
||||||
@@ -272,17 +339,19 @@ Hooks fire **100% of the time**, deterministically. This means:
|
|||||||
|
|
||||||
## Backward Compatibility
|
## Backward Compatibility
|
||||||
|
|
||||||
v2 is fully compatible with v1:
|
v2.1 is fully compatible with v2.0 and v1:
|
||||||
- Existing `~/.claude/skills/learned/` skills still work
|
- Existing global instincts in `~/.claude/homunculus/instincts/` still work as global instincts
|
||||||
|
- Existing `~/.claude/skills/learned/` skills from v1 still work
|
||||||
- Stop hook still runs (but now also feeds into v2)
|
- Stop hook still runs (but now also feeds into v2)
|
||||||
- Gradual migration path: run both in parallel
|
- Gradual migration: run both in parallel
|
||||||
|
|
||||||
## Privacy
|
## Privacy
|
||||||
|
|
||||||
- Observations stay **local** on your machine
|
- Observations stay **local** on your machine
|
||||||
- Only **instincts** (patterns) can be exported
|
- Project-scoped instincts are isolated per project
|
||||||
|
- Only **instincts** (patterns) can be exported — not raw observations
|
||||||
- No actual code or conversation content is shared
|
- No actual code or conversation content is shared
|
||||||
- You control what gets exported
|
- You control what gets exported and promoted
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
@@ -292,4 +361,4 @@ v2 is fully compatible with v1:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Instinct-based learning: teaching Claude your patterns, one observation at a time.*
|
*Instinct-based learning: teaching Claude your patterns, one project at a time.*
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: observer
|
name: observer
|
||||||
description: Background agent that analyzes session observations to detect patterns and create instincts. Uses Haiku for cost-efficiency.
|
description: Background agent that analyzes session observations to detect patterns and create instincts. Uses Haiku for cost-efficiency. v2.1 adds project-scoped instincts.
|
||||||
model: haiku
|
model: haiku
|
||||||
run_mode: background
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Observer Agent
|
# Observer Agent
|
||||||
@@ -11,20 +10,21 @@ A background agent that analyzes observations from Claude Code sessions to detec
|
|||||||
|
|
||||||
## When to Run
|
## When to Run
|
||||||
|
|
||||||
- After significant session activity (20+ tool calls)
|
- After enough observations accumulate (configurable, default 20)
|
||||||
- When user runs `/analyze-patterns`
|
|
||||||
- On a scheduled interval (configurable, default 5 minutes)
|
- On a scheduled interval (configurable, default 5 minutes)
|
||||||
- When triggered by observation hook (SIGUSR1)
|
- When triggered on demand via SIGUSR1 to the observer process
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
|
|
||||||
Reads observations from `~/.claude/homunculus/observations.jsonl`:
|
Reads observations from the **project-scoped** observations file:
|
||||||
|
- Project: `~/.claude/homunculus/projects/<project-hash>/observations.jsonl`
|
||||||
|
- Global fallback: `~/.claude/homunculus/observations.jsonl`
|
||||||
|
|
||||||
```jsonl
|
```jsonl
|
||||||
{"timestamp":"2025-01-22T10:30:00Z","event":"tool_start","session":"abc123","tool":"Edit","input":"..."}
|
{"timestamp":"2025-01-22T10:30:00Z","event":"tool_start","session":"abc123","tool":"Edit","input":"...","project_id":"a1b2c3d4e5f6","project_name":"my-react-app"}
|
||||||
{"timestamp":"2025-01-22T10:30:01Z","event":"tool_complete","session":"abc123","tool":"Edit","output":"..."}
|
{"timestamp":"2025-01-22T10:30:01Z","event":"tool_complete","session":"abc123","tool":"Edit","output":"...","project_id":"a1b2c3d4e5f6","project_name":"my-react-app"}
|
||||||
{"timestamp":"2025-01-22T10:30:05Z","event":"tool_start","session":"abc123","tool":"Bash","input":"npm test"}
|
{"timestamp":"2025-01-22T10:30:05Z","event":"tool_start","session":"abc123","tool":"Bash","input":"npm test","project_id":"a1b2c3d4e5f6","project_name":"my-react-app"}
|
||||||
{"timestamp":"2025-01-22T10:30:10Z","event":"tool_complete","session":"abc123","tool":"Bash","output":"All tests pass"}
|
{"timestamp":"2025-01-22T10:30:10Z","event":"tool_complete","session":"abc123","tool":"Bash","output":"All tests pass","project_id":"a1b2c3d4e5f6","project_name":"my-react-app"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pattern Detection
|
## Pattern Detection
|
||||||
@@ -65,28 +65,75 @@ When certain tools are consistently preferred:
|
|||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
Creates/updates instincts in `~/.claude/homunculus/instincts/personal/`:
|
Creates/updates instincts in the **project-scoped** instincts directory:
|
||||||
|
- Project: `~/.claude/homunculus/projects/<project-hash>/instincts/personal/`
|
||||||
|
- Global: `~/.claude/homunculus/instincts/personal/` (for universal patterns)
|
||||||
|
|
||||||
|
### Project-Scoped Instinct (default)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
id: prefer-grep-before-edit
|
id: use-react-hooks-pattern
|
||||||
trigger: "when searching for code to modify"
|
trigger: "when creating React components"
|
||||||
confidence: 0.65
|
confidence: 0.65
|
||||||
domain: "workflow"
|
domain: "code-style"
|
||||||
source: "session-observation"
|
source: "session-observation"
|
||||||
|
scope: project
|
||||||
|
project_id: "a1b2c3d4e5f6"
|
||||||
|
project_name: "my-react-app"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Prefer Grep Before Edit
|
# Use React Hooks Pattern
|
||||||
|
|
||||||
## Action
|
## Action
|
||||||
Always use Grep to find the exact location before using Edit.
|
Always use functional components with hooks instead of class components.
|
||||||
|
|
||||||
## Evidence
|
## Evidence
|
||||||
- Observed 8 times in session abc123
|
- Observed 8 times in session abc123
|
||||||
- Pattern: Grep → Read → Edit sequence
|
- Pattern: All new components use useState/useEffect
|
||||||
- Last observed: 2025-01-22
|
- Last observed: 2025-01-22
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Global Instinct (universal patterns)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: always-validate-user-input
|
||||||
|
trigger: "when handling user input"
|
||||||
|
confidence: 0.75
|
||||||
|
domain: "security"
|
||||||
|
source: "session-observation"
|
||||||
|
scope: global
|
||||||
|
---
|
||||||
|
|
||||||
|
# Always Validate User Input
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Validate and sanitize all user input before processing.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
- Observed across 3 different projects
|
||||||
|
- Pattern: User consistently adds input validation
|
||||||
|
- Last observed: 2025-01-22
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scope Decision Guide
|
||||||
|
|
||||||
|
When creating instincts, determine scope based on these heuristics:
|
||||||
|
|
||||||
|
| Pattern Type | Scope | Examples |
|
||||||
|
|-------------|-------|---------|
|
||||||
|
| Language/framework conventions | **project** | "Use React hooks", "Follow Django REST patterns" |
|
||||||
|
| File structure preferences | **project** | "Tests in __tests__/", "Components in src/components/" |
|
||||||
|
| Code style | **project** | "Use functional style", "Prefer dataclasses" |
|
||||||
|
| Error handling strategies | **project** (usually) | "Use Result type for errors" |
|
||||||
|
| Security practices | **global** | "Validate user input", "Sanitize SQL" |
|
||||||
|
| General best practices | **global** | "Write tests first", "Always handle errors" |
|
||||||
|
| Tool workflow preferences | **global** | "Grep before Edit", "Read before Write" |
|
||||||
|
| Git practices | **global** | "Conventional commits", "Small focused commits" |
|
||||||
|
|
||||||
|
**When in doubt, default to `scope: project`** — it's safer to be project-specific and promote later than to contaminate the global space.
|
||||||
|
|
||||||
## Confidence Calculation
|
## Confidence Calculation
|
||||||
|
|
||||||
Initial confidence based on observation frequency:
|
Initial confidence based on observation frequency:
|
||||||
@@ -100,6 +147,15 @@ Confidence adjusts over time:
|
|||||||
- -0.1 for each contradicting observation
|
- -0.1 for each contradicting observation
|
||||||
- -0.02 per week without observation (decay)
|
- -0.02 per week without observation (decay)
|
||||||
|
|
||||||
|
## Instinct Promotion (Project → Global)
|
||||||
|
|
||||||
|
An instinct should be promoted from project-scoped to global when:
|
||||||
|
1. The **same pattern** (by id or similar trigger) exists in **2+ different projects**
|
||||||
|
2. Each instance has confidence **>= 0.8**
|
||||||
|
3. The domain is in the global-friendly list (security, general-best-practices, workflow)
|
||||||
|
|
||||||
|
Promotion is handled by the `instinct-cli.py promote` command or the `/evolve` analysis.
|
||||||
|
|
||||||
## Important Guidelines
|
## Important Guidelines
|
||||||
|
|
||||||
1. **Be Conservative**: Only create instincts for clear patterns (3+ observations)
|
1. **Be Conservative**: Only create instincts for clear patterns (3+ observations)
|
||||||
@@ -107,31 +163,36 @@ Confidence adjusts over time:
|
|||||||
3. **Track Evidence**: Always include what observations led to the instinct
|
3. **Track Evidence**: Always include what observations led to the instinct
|
||||||
4. **Respect Privacy**: Never include actual code snippets, only patterns
|
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
|
5. **Merge Similar**: If a new instinct is similar to existing, update rather than duplicate
|
||||||
|
6. **Default to Project Scope**: Unless the pattern is clearly universal, make it project-scoped
|
||||||
|
7. **Include Project Context**: Always set `project_id` and `project_name` for project-scoped instincts
|
||||||
|
|
||||||
## Example Analysis Session
|
## Example Analysis Session
|
||||||
|
|
||||||
Given observations:
|
Given observations:
|
||||||
```jsonl
|
```jsonl
|
||||||
{"event":"tool_start","tool":"Grep","input":"pattern: useState"}
|
{"event":"tool_start","tool":"Grep","input":"pattern: useState","project_id":"a1b2c3","project_name":"my-app"}
|
||||||
{"event":"tool_complete","tool":"Grep","output":"Found in 3 files"}
|
{"event":"tool_complete","tool":"Grep","output":"Found in 3 files","project_id":"a1b2c3","project_name":"my-app"}
|
||||||
{"event":"tool_start","tool":"Read","input":"src/hooks/useAuth.ts"}
|
{"event":"tool_start","tool":"Read","input":"src/hooks/useAuth.ts","project_id":"a1b2c3","project_name":"my-app"}
|
||||||
{"event":"tool_complete","tool":"Read","output":"[file content]"}
|
{"event":"tool_complete","tool":"Read","output":"[file content]","project_id":"a1b2c3","project_name":"my-app"}
|
||||||
{"event":"tool_start","tool":"Edit","input":"src/hooks/useAuth.ts..."}
|
{"event":"tool_start","tool":"Edit","input":"src/hooks/useAuth.ts...","project_id":"a1b2c3","project_name":"my-app"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Analysis:
|
Analysis:
|
||||||
- Detected workflow: Grep → Read → Edit
|
- Detected workflow: Grep → Read → Edit
|
||||||
- Frequency: Seen 5 times this session
|
- Frequency: Seen 5 times this session
|
||||||
|
- **Scope decision**: This is a general workflow pattern (not project-specific) → **global**
|
||||||
- Create instinct:
|
- Create instinct:
|
||||||
- trigger: "when modifying code"
|
- trigger: "when modifying code"
|
||||||
- action: "Search with Grep, confirm with Read, then Edit"
|
- action: "Search with Grep, confirm with Read, then Edit"
|
||||||
- confidence: 0.6
|
- confidence: 0.6
|
||||||
- domain: "workflow"
|
- domain: "workflow"
|
||||||
|
- scope: "global"
|
||||||
|
|
||||||
## Integration with Skill Creator
|
## Integration with Skill Creator
|
||||||
|
|
||||||
When instincts are imported from Skill Creator (repo analysis), they have:
|
When instincts are imported from Skill Creator (repo analysis), they have:
|
||||||
- `source: "repo-analysis"`
|
- `source: "repo-analysis"`
|
||||||
- `source_repo: "https://github.com/..."`
|
- `source_repo: "https://github.com/..."`
|
||||||
|
- `scope: "project"` (since they come from a specific repo)
|
||||||
|
|
||||||
These should be treated as team/project conventions with higher initial confidence (0.7+).
|
These should be treated as team/project conventions with higher initial confidence (0.7+).
|
||||||
|
|||||||
@@ -4,26 +4,79 @@
|
|||||||
# Starts the background observer agent that analyzes observations
|
# Starts the background observer agent that analyzes observations
|
||||||
# and creates instincts. Uses Haiku model for cost efficiency.
|
# and creates instincts. Uses Haiku model for cost efficiency.
|
||||||
#
|
#
|
||||||
|
# v2.1: Project-scoped — detects current project and analyzes
|
||||||
|
# project-specific observations into project-scoped instincts.
|
||||||
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# start-observer.sh # Start observer in background
|
# start-observer.sh # Start observer for current project (or global)
|
||||||
# start-observer.sh stop # Stop running observer
|
# start-observer.sh stop # Stop running observer
|
||||||
# start-observer.sh status # Check if observer is running
|
# start-observer.sh status # Check if observer is running
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
CONFIG_DIR="${HOME}/.claude/homunculus"
|
# ─────────────────────────────────────────────
|
||||||
PID_FILE="${CONFIG_DIR}/.observer.pid"
|
# Project detection
|
||||||
LOG_FILE="${CONFIG_DIR}/observer.log"
|
# ─────────────────────────────────────────────
|
||||||
OBSERVATIONS_FILE="${CONFIG_DIR}/observations.jsonl"
|
|
||||||
|
|
||||||
mkdir -p "$CONFIG_DIR"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
# Source shared project detection helper
|
||||||
|
# This sets: PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
||||||
|
source "${SKILL_ROOT}/scripts/detect-project.sh"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Configuration
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
CONFIG_DIR="${HOME}/.claude/homunculus"
|
||||||
|
CONFIG_FILE="${SKILL_ROOT}/config.json"
|
||||||
|
# PID file is project-scoped so each project can have its own observer
|
||||||
|
PID_FILE="${PROJECT_DIR}/.observer.pid"
|
||||||
|
LOG_FILE="${PROJECT_DIR}/observer.log"
|
||||||
|
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
|
||||||
|
INSTINCTS_DIR="${PROJECT_DIR}/instincts/personal"
|
||||||
|
|
||||||
|
# Read config values from config.json
|
||||||
|
OBSERVER_INTERVAL_MINUTES=5
|
||||||
|
MIN_OBSERVATIONS=20
|
||||||
|
OBSERVER_ENABLED=false
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
_config=$(CLV2_CONFIG="$CONFIG_FILE" python3 -c "
|
||||||
|
import json, os
|
||||||
|
with open(os.environ['CLV2_CONFIG']) as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
obs = cfg.get('observer', {})
|
||||||
|
print(obs.get('run_interval_minutes', 5))
|
||||||
|
print(obs.get('min_observations_to_analyze', 20))
|
||||||
|
print(str(obs.get('enabled', False)).lower())
|
||||||
|
" 2>/dev/null || echo "5
|
||||||
|
20
|
||||||
|
false")
|
||||||
|
_interval=$(echo "$_config" | sed -n '1p')
|
||||||
|
_min_obs=$(echo "$_config" | sed -n '2p')
|
||||||
|
_enabled=$(echo "$_config" | sed -n '3p')
|
||||||
|
if [ "$_interval" -gt 0 ] 2>/dev/null; then
|
||||||
|
OBSERVER_INTERVAL_MINUTES="$_interval"
|
||||||
|
fi
|
||||||
|
if [ "$_min_obs" -gt 0 ] 2>/dev/null; then
|
||||||
|
MIN_OBSERVATIONS="$_min_obs"
|
||||||
|
fi
|
||||||
|
if [ "$_enabled" = "true" ]; then
|
||||||
|
OBSERVER_ENABLED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
OBSERVER_INTERVAL_SECONDS=$((OBSERVER_INTERVAL_MINUTES * 60))
|
||||||
|
|
||||||
|
echo "Project: ${PROJECT_NAME} (${PROJECT_ID})"
|
||||||
|
echo "Storage: ${PROJECT_DIR}"
|
||||||
|
|
||||||
case "${1:-start}" in
|
case "${1:-start}" in
|
||||||
stop)
|
stop)
|
||||||
if [ -f "$PID_FILE" ]; then
|
if [ -f "$PID_FILE" ]; then
|
||||||
pid=$(cat "$PID_FILE")
|
pid=$(cat "$PID_FILE")
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
echo "Stopping observer (PID: $pid)..."
|
echo "Stopping observer for ${PROJECT_NAME} (PID: $pid)..."
|
||||||
kill "$pid"
|
kill "$pid"
|
||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
echo "Observer stopped."
|
echo "Observer stopped."
|
||||||
@@ -44,6 +97,9 @@ case "${1:-start}" in
|
|||||||
echo "Observer is running (PID: $pid)"
|
echo "Observer is running (PID: $pid)"
|
||||||
echo "Log: $LOG_FILE"
|
echo "Log: $LOG_FILE"
|
||||||
echo "Observations: $(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0) lines"
|
echo "Observations: $(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0) lines"
|
||||||
|
# Also show instinct count
|
||||||
|
instinct_count=$(find "$INSTINCTS_DIR" -name "*.yaml" 2>/dev/null | wc -l)
|
||||||
|
echo "Instincts: $instinct_count"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo "Observer not running (stale PID file)"
|
echo "Observer not running (stale PID file)"
|
||||||
@@ -57,17 +113,24 @@ case "${1:-start}" in
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
start)
|
start)
|
||||||
|
# Check if observer is disabled in config
|
||||||
|
if [ "$OBSERVER_ENABLED" != "true" ]; then
|
||||||
|
echo "Observer is disabled in config.json (observer.enabled: false)."
|
||||||
|
echo "Set observer.enabled to true in config.json to enable."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if already running
|
# Check if already running
|
||||||
if [ -f "$PID_FILE" ]; then
|
if [ -f "$PID_FILE" ]; then
|
||||||
pid=$(cat "$PID_FILE")
|
pid=$(cat "$PID_FILE")
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
echo "Observer already running (PID: $pid)"
|
echo "Observer already running for ${PROJECT_NAME} (PID: $pid)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting observer agent..."
|
echo "Starting observer agent for ${PROJECT_NAME}..."
|
||||||
|
|
||||||
# The observer loop
|
# The observer loop
|
||||||
(
|
(
|
||||||
@@ -79,18 +142,43 @@ case "${1:-start}" in
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
obs_count=$(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0)
|
obs_count=$(wc -l < "$OBSERVATIONS_FILE" 2>/dev/null || echo 0)
|
||||||
if [ "$obs_count" -lt 10 ]; then
|
if [ "$obs_count" -lt "$MIN_OBSERVATIONS" ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[$(date)] Analyzing $obs_count observations..." >> "$LOG_FILE"
|
echo "[$(date)] Analyzing $obs_count observations for project ${PROJECT_NAME}..." >> "$LOG_FILE"
|
||||||
|
|
||||||
# Use Claude Code with Haiku to analyze observations
|
# Use Claude Code with Haiku to analyze observations
|
||||||
# This spawns a quick analysis session
|
# The prompt now specifies project-scoped instinct creation
|
||||||
if command -v claude &> /dev/null; then
|
if command -v claude &> /dev/null; then
|
||||||
exit_code=0
|
exit_code=0
|
||||||
claude --model haiku --max-turns 3 --print \
|
claude --model haiku --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." \
|
"Read $OBSERVATIONS_FILE and identify patterns for the project '${PROJECT_NAME}'.
|
||||||
|
If you find 3+ occurrences of the same pattern, create an instinct file in $INSTINCTS_DIR/ following this format:
|
||||||
|
|
||||||
|
---
|
||||||
|
id: <kebab-case-id>
|
||||||
|
trigger: \"<when this happens>\"
|
||||||
|
confidence: <0.3-0.9>
|
||||||
|
domain: <code-style|testing|git|debugging|workflow|etc>
|
||||||
|
source: session-observation
|
||||||
|
scope: project
|
||||||
|
project_id: ${PROJECT_ID}
|
||||||
|
project_name: ${PROJECT_NAME}
|
||||||
|
---
|
||||||
|
|
||||||
|
# <Title>
|
||||||
|
|
||||||
|
## Action
|
||||||
|
<What to do>
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
<What observations led to this>
|
||||||
|
|
||||||
|
Be conservative - only create instincts for clear patterns.
|
||||||
|
If a pattern seems universal (not project-specific), set scope to 'global' instead of 'project'.
|
||||||
|
Examples of global patterns: 'always validate user input', 'prefer explicit error handling'.
|
||||||
|
Examples of project patterns: 'use React functional components', 'follow Django REST framework conventions'." \
|
||||||
>> "$LOG_FILE" 2>&1 || exit_code=$?
|
>> "$LOG_FILE" 2>&1 || exit_code=$?
|
||||||
if [ "$exit_code" -ne 0 ]; then
|
if [ "$exit_code" -ne 0 ]; then
|
||||||
echo "[$(date)] Claude analysis failed (exit $exit_code)" >> "$LOG_FILE"
|
echo "[$(date)] Claude analysis failed (exit $exit_code)" >> "$LOG_FILE"
|
||||||
@@ -101,10 +189,9 @@ case "${1:-start}" in
|
|||||||
|
|
||||||
# Archive processed observations
|
# Archive processed observations
|
||||||
if [ -f "$OBSERVATIONS_FILE" ]; then
|
if [ -f "$OBSERVATIONS_FILE" ]; then
|
||||||
archive_dir="${CONFIG_DIR}/observations.archive"
|
archive_dir="${PROJECT_DIR}/observations.archive"
|
||||||
mkdir -p "$archive_dir"
|
mkdir -p "$archive_dir"
|
||||||
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S).jsonl" 2>/dev/null || true
|
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S)-$$.jsonl" 2>/dev/null || true
|
||||||
touch "$OBSERVATIONS_FILE"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +199,11 @@ case "${1:-start}" in
|
|||||||
trap 'analyze_observations' USR1
|
trap 'analyze_observations' USR1
|
||||||
|
|
||||||
echo "$$" > "$PID_FILE"
|
echo "$$" > "$PID_FILE"
|
||||||
echo "[$(date)] Observer started (PID: $$)" >> "$LOG_FILE"
|
echo "[$(date)] Observer started for ${PROJECT_NAME} (PID: $$)" >> "$LOG_FILE"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# Check every 5 minutes
|
# Check at configured interval (default: 5 minutes)
|
||||||
sleep 300
|
sleep "$OBSERVER_INTERVAL_SECONDS"
|
||||||
|
|
||||||
analyze_observations
|
analyze_observations
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,41 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0",
|
"version": "2.1",
|
||||||
"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": {
|
"observer": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"model": "haiku",
|
|
||||||
"run_interval_minutes": 5,
|
"run_interval_minutes": 5,
|
||||||
"min_observations_to_analyze": 20,
|
"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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,52 +4,20 @@
|
|||||||
# Captures tool use events for pattern analysis.
|
# Captures tool use events for pattern analysis.
|
||||||
# Claude Code passes hook data via stdin as JSON.
|
# Claude Code passes hook data via stdin as JSON.
|
||||||
#
|
#
|
||||||
# Hook config (in ~/.claude/settings.json):
|
# v2.1: Project-scoped observations — detects current project context
|
||||||
|
# and writes observations to project-specific directory.
|
||||||
#
|
#
|
||||||
# If installed as a plugin, use ${CLAUDE_PLUGIN_ROOT}:
|
# Registered via plugin hooks/hooks.json (auto-loaded when plugin is enabled).
|
||||||
# {
|
# Can also be registered manually in ~/.claude/settings.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:
|
|
||||||
# {
|
|
||||||
# "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
|
set -e
|
||||||
|
|
||||||
# Hook phase from CLI argument: "pre" (PreToolUse) or "post" (PostToolUse)
|
# Hook phase from CLI argument: "pre" (PreToolUse) or "post" (PostToolUse)
|
||||||
HOOK_PHASE="${1:-post}"
|
HOOK_PHASE="${1:-post}"
|
||||||
|
|
||||||
CONFIG_DIR="${HOME}/.claude/homunculus"
|
# ─────────────────────────────────────────────
|
||||||
OBSERVATIONS_FILE="${CONFIG_DIR}/observations.jsonl"
|
# Read stdin first (before project detection)
|
||||||
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)
|
# Read JSON from stdin (Claude Code hook format)
|
||||||
INPUT_JSON=$(cat)
|
INPUT_JSON=$(cat)
|
||||||
@@ -59,6 +27,51 @@ if [ -z "$INPUT_JSON" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Extract cwd from stdin for project detection
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Extract cwd from the hook JSON to use for project detection.
|
||||||
|
# This avoids spawning a separate git subprocess when cwd is available.
|
||||||
|
STDIN_CWD=$(echo "$INPUT_JSON" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
cwd = data.get("cwd", "")
|
||||||
|
print(cwd)
|
||||||
|
except(KeyError, TypeError, ValueError):
|
||||||
|
print("")
|
||||||
|
' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# If cwd was provided in stdin, use it for project detection
|
||||||
|
if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then
|
||||||
|
export CLAUDE_PROJECT_DIR="$STDIN_CWD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Project detection
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
# Source shared project detection helper
|
||||||
|
# This sets: PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
||||||
|
source "${SKILL_ROOT}/scripts/detect-project.sh"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Configuration
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
CONFIG_DIR="${HOME}/.claude/homunculus"
|
||||||
|
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
|
||||||
|
MAX_FILE_SIZE_MB=10
|
||||||
|
|
||||||
|
# Skip if disabled
|
||||||
|
if [ -f "$CONFIG_DIR/disabled" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Parse using python via stdin pipe (safe for all JSON payloads)
|
# Parse using python via stdin pipe (safe for all JSON payloads)
|
||||||
# Pass HOOK_PHASE via env var since Claude Code does not include hook type in stdin JSON
|
# Pass HOOK_PHASE via env var since Claude Code does not include hook type in stdin JSON
|
||||||
PARSED=$(echo "$INPUT_JSON" | HOOK_PHASE="$HOOK_PHASE" python3 -c '
|
PARSED=$(echo "$INPUT_JSON" | HOOK_PHASE="$HOOK_PHASE" python3 -c '
|
||||||
@@ -80,6 +93,8 @@ try:
|
|||||||
tool_input = data.get("tool_input", data.get("input", {}))
|
tool_input = data.get("tool_input", data.get("input", {}))
|
||||||
tool_output = data.get("tool_output", data.get("output", ""))
|
tool_output = data.get("tool_output", data.get("output", ""))
|
||||||
session_id = data.get("session_id", "unknown")
|
session_id = data.get("session_id", "unknown")
|
||||||
|
tool_use_id = data.get("tool_use_id", "")
|
||||||
|
cwd = data.get("cwd", "")
|
||||||
|
|
||||||
# Truncate large inputs/outputs
|
# Truncate large inputs/outputs
|
||||||
if isinstance(tool_input, dict):
|
if isinstance(tool_input, dict):
|
||||||
@@ -88,24 +103,26 @@ try:
|
|||||||
tool_input_str = str(tool_input)[:5000]
|
tool_input_str = str(tool_input)[:5000]
|
||||||
|
|
||||||
if isinstance(tool_output, dict):
|
if isinstance(tool_output, dict):
|
||||||
tool_output_str = json.dumps(tool_output)[:5000]
|
tool_response_str = json.dumps(tool_output)[:5000]
|
||||||
else:
|
else:
|
||||||
tool_output_str = str(tool_output)[:5000]
|
tool_response_str = str(tool_output)[:5000]
|
||||||
|
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"parsed": True,
|
"parsed": True,
|
||||||
"event": event,
|
"event": event,
|
||||||
"tool": tool_name,
|
"tool": tool_name,
|
||||||
"input": tool_input_str if event == "tool_start" else None,
|
"input": tool_input_str if event == "tool_start" else None,
|
||||||
"output": tool_output_str if event == "tool_complete" else None,
|
"output": tool_response_str if event == "tool_complete" else None,
|
||||||
"session": session_id
|
"session": session_id,
|
||||||
|
"tool_use_id": tool_use_id,
|
||||||
|
"cwd": cwd
|
||||||
}))
|
}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(json.dumps({"parsed": False, "error": str(e)}))
|
print(json.dumps({"parsed": False, "error": str(e)}))
|
||||||
')
|
')
|
||||||
|
|
||||||
# Check if parsing succeeded
|
# Check if parsing succeeded
|
||||||
PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.stdin).get('parsed', False))")
|
PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.stdin).get('parsed', False))" 2>/dev/null || echo "False")
|
||||||
|
|
||||||
if [ "$PARSED_OK" != "True" ]; then
|
if [ "$PARSED_OK" != "True" ]; then
|
||||||
# Fallback: log raw input for debugging
|
# Fallback: log raw input for debugging
|
||||||
@@ -119,20 +136,23 @@ print(json.dumps({'timestamp': os.environ['TIMESTAMP'], 'event': 'parse_error',
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Archive if file too large
|
# Archive if file too large (atomic: rename with unique suffix to avoid race)
|
||||||
if [ -f "$OBSERVATIONS_FILE" ]; then
|
if [ -f "$OBSERVATIONS_FILE" ]; then
|
||||||
file_size_mb=$(du -m "$OBSERVATIONS_FILE" 2>/dev/null | cut -f1)
|
file_size_mb=$(du -m "$OBSERVATIONS_FILE" 2>/dev/null | cut -f1)
|
||||||
if [ "${file_size_mb:-0}" -ge "$MAX_FILE_SIZE_MB" ]; then
|
if [ "${file_size_mb:-0}" -ge "$MAX_FILE_SIZE_MB" ]; then
|
||||||
archive_dir="${CONFIG_DIR}/observations.archive"
|
archive_dir="${PROJECT_DIR}/observations.archive"
|
||||||
mkdir -p "$archive_dir"
|
mkdir -p "$archive_dir"
|
||||||
mv "$OBSERVATIONS_FILE" "$archive_dir/observations-$(date +%Y%m%d-%H%M%S).jsonl"
|
mv "$OBSERVATIONS_FILE" "$archive_dir/observations-$(date +%Y%m%d-%H%M%S)-$$.jsonl" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build and write observation
|
# Build and write observation (now includes project context)
|
||||||
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
export PROJECT_ID_ENV="$PROJECT_ID"
|
||||||
|
export PROJECT_NAME_ENV="$PROJECT_NAME"
|
||||||
export TIMESTAMP="$timestamp"
|
export TIMESTAMP="$timestamp"
|
||||||
|
|
||||||
echo "$PARSED" | python3 -c "
|
echo "$PARSED" | python3 -c "
|
||||||
import json, sys, os
|
import json, sys, os
|
||||||
|
|
||||||
@@ -141,10 +161,12 @@ observation = {
|
|||||||
'timestamp': os.environ['TIMESTAMP'],
|
'timestamp': os.environ['TIMESTAMP'],
|
||||||
'event': parsed['event'],
|
'event': parsed['event'],
|
||||||
'tool': parsed['tool'],
|
'tool': parsed['tool'],
|
||||||
'session': parsed['session']
|
'session': parsed['session'],
|
||||||
|
'project_id': os.environ.get('PROJECT_ID_ENV', 'global'),
|
||||||
|
'project_name': os.environ.get('PROJECT_NAME_ENV', 'global')
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsed['input'] is not None:
|
if parsed['input']:
|
||||||
observation['input'] = parsed['input']
|
observation['input'] = parsed['input']
|
||||||
if parsed['output'] is not None:
|
if parsed['output'] is not None:
|
||||||
observation['output'] = parsed['output']
|
observation['output'] = parsed['output']
|
||||||
@@ -152,13 +174,14 @@ if parsed['output'] is not None:
|
|||||||
print(json.dumps(observation))
|
print(json.dumps(observation))
|
||||||
" >> "$OBSERVATIONS_FILE"
|
" >> "$OBSERVATIONS_FILE"
|
||||||
|
|
||||||
# Signal observer if running
|
# Signal observer if running (check both project-scoped and global observer)
|
||||||
OBSERVER_PID_FILE="${CONFIG_DIR}/.observer.pid"
|
for pid_file in "${PROJECT_DIR}/.observer.pid" "${CONFIG_DIR}/.observer.pid"; do
|
||||||
if [ -f "$OBSERVER_PID_FILE" ]; then
|
if [ -f "$pid_file" ]; then
|
||||||
observer_pid=$(cat "$OBSERVER_PID_FILE")
|
observer_pid=$(cat "$pid_file")
|
||||||
if kill -0 "$observer_pid" 2>/dev/null; then
|
if kill -0 "$observer_pid" 2>/dev/null; then
|
||||||
kill -USR1 "$observer_pid" 2>/dev/null || true
|
kill -USR1 "$observer_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
done
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
141
skills/continuous-learning-v2/scripts/detect-project.sh
Executable file
141
skills/continuous-learning-v2/scripts/detect-project.sh
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Continuous Learning v2 - Project Detection Helper
|
||||||
|
#
|
||||||
|
# Shared logic for detecting current project context.
|
||||||
|
# Sourced by observe.sh and start-observer.sh.
|
||||||
|
#
|
||||||
|
# Exports:
|
||||||
|
# _CLV2_PROJECT_ID - Short hash identifying the project (or "global")
|
||||||
|
# _CLV2_PROJECT_NAME - Human-readable project name
|
||||||
|
# _CLV2_PROJECT_ROOT - Absolute path to project root
|
||||||
|
# _CLV2_PROJECT_DIR - Project-scoped storage directory under homunculus
|
||||||
|
#
|
||||||
|
# Also sets unprefixed convenience aliases:
|
||||||
|
# PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
||||||
|
#
|
||||||
|
# Detection priority:
|
||||||
|
# 1. CLAUDE_PROJECT_DIR env var (if set)
|
||||||
|
# 2. git remote URL (hashed for uniqueness across machines)
|
||||||
|
# 3. git repo root path (fallback, machine-specific)
|
||||||
|
# 4. "global" (no project context detected)
|
||||||
|
|
||||||
|
_CLV2_HOMUNCULUS_DIR="${HOME}/.claude/homunculus"
|
||||||
|
_CLV2_PROJECTS_DIR="${_CLV2_HOMUNCULUS_DIR}/projects"
|
||||||
|
_CLV2_REGISTRY_FILE="${_CLV2_HOMUNCULUS_DIR}/projects.json"
|
||||||
|
|
||||||
|
_clv2_detect_project() {
|
||||||
|
local project_root=""
|
||||||
|
local project_name=""
|
||||||
|
local project_id=""
|
||||||
|
local source_hint=""
|
||||||
|
|
||||||
|
# 1. Try CLAUDE_PROJECT_DIR env var
|
||||||
|
if [ -n "$CLAUDE_PROJECT_DIR" ] && [ -d "$CLAUDE_PROJECT_DIR" ]; then
|
||||||
|
project_root="$CLAUDE_PROJECT_DIR"
|
||||||
|
source_hint="env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Try git repo root from CWD (only if git is available)
|
||||||
|
if [ -z "$project_root" ] && command -v git &>/dev/null; then
|
||||||
|
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
if [ -n "$project_root" ]; then
|
||||||
|
source_hint="git"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. No project detected — fall back to global
|
||||||
|
if [ -z "$project_root" ]; then
|
||||||
|
_CLV2_PROJECT_ID="global"
|
||||||
|
_CLV2_PROJECT_NAME="global"
|
||||||
|
_CLV2_PROJECT_ROOT=""
|
||||||
|
_CLV2_PROJECT_DIR="${_CLV2_HOMUNCULUS_DIR}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Derive project name from directory basename
|
||||||
|
project_name=$(basename "$project_root")
|
||||||
|
|
||||||
|
# Derive project ID: prefer git remote URL hash (portable across machines),
|
||||||
|
# fall back to path hash (machine-specific but still useful)
|
||||||
|
local remote_url=""
|
||||||
|
if command -v git &>/dev/null; then
|
||||||
|
if [ "$source_hint" = "git" ] || [ -d "${project_root}/.git" ]; then
|
||||||
|
remote_url=$(git -C "$project_root" remote get-url origin 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local hash_input="${remote_url:-$project_root}"
|
||||||
|
# Use SHA256 via python3 (portable across macOS/Linux, no shasum/sha256sum divergence)
|
||||||
|
project_id=$(printf '%s' "$hash_input" | python3 -c "import sys,hashlib; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:12])" 2>/dev/null)
|
||||||
|
|
||||||
|
# Fallback if python3 failed
|
||||||
|
if [ -z "$project_id" ]; then
|
||||||
|
project_id=$(printf '%s' "$hash_input" | shasum -a 256 2>/dev/null | cut -c1-12 || \
|
||||||
|
printf '%s' "$hash_input" | sha256sum 2>/dev/null | cut -c1-12 || \
|
||||||
|
echo "fallback")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
_CLV2_PROJECT_ID="$project_id"
|
||||||
|
_CLV2_PROJECT_NAME="$project_name"
|
||||||
|
_CLV2_PROJECT_ROOT="$project_root"
|
||||||
|
_CLV2_PROJECT_DIR="${_CLV2_PROJECTS_DIR}/${project_id}"
|
||||||
|
|
||||||
|
# Ensure project directory structure exists
|
||||||
|
mkdir -p "${_CLV2_PROJECT_DIR}/instincts/personal"
|
||||||
|
mkdir -p "${_CLV2_PROJECT_DIR}/instincts/inherited"
|
||||||
|
mkdir -p "${_CLV2_PROJECT_DIR}/observations.archive"
|
||||||
|
mkdir -p "${_CLV2_PROJECT_DIR}/evolved/skills"
|
||||||
|
mkdir -p "${_CLV2_PROJECT_DIR}/evolved/commands"
|
||||||
|
mkdir -p "${_CLV2_PROJECT_DIR}/evolved/agents"
|
||||||
|
|
||||||
|
# Update project registry (lightweight JSON mapping)
|
||||||
|
_clv2_update_project_registry "$project_id" "$project_name" "$project_root" "$remote_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
_clv2_update_project_registry() {
|
||||||
|
local pid="$1"
|
||||||
|
local pname="$2"
|
||||||
|
local proot="$3"
|
||||||
|
local premote="$4"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$_CLV2_REGISTRY_FILE")"
|
||||||
|
|
||||||
|
# Pass values via env vars to avoid shell→python injection.
|
||||||
|
# python3 reads them with os.environ, which is safe for any string content.
|
||||||
|
_CLV2_REG_PID="$pid" \
|
||||||
|
_CLV2_REG_PNAME="$pname" \
|
||||||
|
_CLV2_REG_PROOT="$proot" \
|
||||||
|
_CLV2_REG_PREMOTE="$premote" \
|
||||||
|
_CLV2_REG_FILE="$_CLV2_REGISTRY_FILE" \
|
||||||
|
python3 -c '
|
||||||
|
import json, os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
registry_path = os.environ["_CLV2_REG_FILE"]
|
||||||
|
try:
|
||||||
|
with open(registry_path) as f:
|
||||||
|
registry = json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
registry = {}
|
||||||
|
|
||||||
|
registry[os.environ["_CLV2_REG_PID"]] = {
|
||||||
|
"name": os.environ["_CLV2_REG_PNAME"],
|
||||||
|
"root": os.environ["_CLV2_REG_PROOT"],
|
||||||
|
"remote": os.environ["_CLV2_REG_PREMOTE"],
|
||||||
|
"last_seen": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(registry_path, "w") as f:
|
||||||
|
json.dump(registry, f, indent=2)
|
||||||
|
' 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-detect on source
|
||||||
|
_clv2_detect_project
|
||||||
|
|
||||||
|
# Convenience aliases for callers (short names pointing to prefixed vars)
|
||||||
|
PROJECT_ID="$_CLV2_PROJECT_ID"
|
||||||
|
PROJECT_NAME="$_CLV2_PROJECT_NAME"
|
||||||
|
PROJECT_ROOT="$_CLV2_PROJECT_ROOT"
|
||||||
|
PROJECT_DIR="$_CLV2_PROJECT_DIR"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,26 @@
|
|||||||
"""Tests for parse_instinct_file() — verifies content after frontmatter is preserved."""
|
"""Tests for continuous-learning-v2 instinct-cli.py
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- parse_instinct_file() — content preservation, edge cases
|
||||||
|
- _validate_file_path() — path traversal blocking
|
||||||
|
- detect_project() — project detection with mocked git/env
|
||||||
|
- load_all_instincts() — loading from project + global dirs, dedup
|
||||||
|
- _load_instincts_from_dir() — directory scanning
|
||||||
|
- cmd_projects() — listing projects from registry
|
||||||
|
- cmd_status() — status display
|
||||||
|
- _promote_specific() — single instinct promotion
|
||||||
|
- _promote_auto() — auto-promotion across projects
|
||||||
|
"""
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# Load instinct-cli.py (hyphenated filename requires importlib)
|
# Load instinct-cli.py (hyphenated filename requires importlib)
|
||||||
_spec = importlib.util.spec_from_file_location(
|
_spec = importlib.util.spec_from_file_location(
|
||||||
@@ -10,8 +29,125 @@ _spec = importlib.util.spec_from_file_location(
|
|||||||
)
|
)
|
||||||
_mod = importlib.util.module_from_spec(_spec)
|
_mod = importlib.util.module_from_spec(_spec)
|
||||||
_spec.loader.exec_module(_mod)
|
_spec.loader.exec_module(_mod)
|
||||||
parse_instinct_file = _mod.parse_instinct_file
|
|
||||||
|
|
||||||
|
parse_instinct_file = _mod.parse_instinct_file
|
||||||
|
_validate_file_path = _mod._validate_file_path
|
||||||
|
detect_project = _mod.detect_project
|
||||||
|
load_all_instincts = _mod.load_all_instincts
|
||||||
|
load_project_only_instincts = _mod.load_project_only_instincts
|
||||||
|
_load_instincts_from_dir = _mod._load_instincts_from_dir
|
||||||
|
cmd_status = _mod.cmd_status
|
||||||
|
cmd_projects = _mod.cmd_projects
|
||||||
|
_promote_specific = _mod._promote_specific
|
||||||
|
_promote_auto = _mod._promote_auto
|
||||||
|
_find_cross_project_instincts = _mod._find_cross_project_instincts
|
||||||
|
load_registry = _mod.load_registry
|
||||||
|
_validate_instinct_id = _mod._validate_instinct_id
|
||||||
|
_update_registry = _mod._update_registry
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Fixtures
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
SAMPLE_INSTINCT_YAML = """\
|
||||||
|
---
|
||||||
|
id: test-instinct
|
||||||
|
trigger: "when writing tests"
|
||||||
|
confidence: 0.8
|
||||||
|
domain: testing
|
||||||
|
scope: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Always write tests first.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
TDD leads to better design.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SAMPLE_GLOBAL_INSTINCT_YAML = """\
|
||||||
|
---
|
||||||
|
id: global-instinct
|
||||||
|
trigger: "always"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: security
|
||||||
|
scope: global
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Validate all user input.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def project_tree(tmp_path):
|
||||||
|
"""Create a realistic project directory tree for testing."""
|
||||||
|
homunculus = tmp_path / ".claude" / "homunculus"
|
||||||
|
projects_dir = homunculus / "projects"
|
||||||
|
global_personal = homunculus / "instincts" / "personal"
|
||||||
|
global_inherited = homunculus / "instincts" / "inherited"
|
||||||
|
global_evolved = homunculus / "evolved"
|
||||||
|
|
||||||
|
for d in [
|
||||||
|
global_personal, global_inherited,
|
||||||
|
global_evolved / "skills", global_evolved / "commands", global_evolved / "agents",
|
||||||
|
projects_dir,
|
||||||
|
]:
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"root": tmp_path,
|
||||||
|
"homunculus": homunculus,
|
||||||
|
"projects_dir": projects_dir,
|
||||||
|
"global_personal": global_personal,
|
||||||
|
"global_inherited": global_inherited,
|
||||||
|
"global_evolved": global_evolved,
|
||||||
|
"registry_file": homunculus / "projects.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def patch_globals(project_tree, monkeypatch):
|
||||||
|
"""Patch module-level globals to use tmp_path-based directories."""
|
||||||
|
monkeypatch.setattr(_mod, "HOMUNCULUS_DIR", project_tree["homunculus"])
|
||||||
|
monkeypatch.setattr(_mod, "PROJECTS_DIR", project_tree["projects_dir"])
|
||||||
|
monkeypatch.setattr(_mod, "REGISTRY_FILE", project_tree["registry_file"])
|
||||||
|
monkeypatch.setattr(_mod, "GLOBAL_PERSONAL_DIR", project_tree["global_personal"])
|
||||||
|
monkeypatch.setattr(_mod, "GLOBAL_INHERITED_DIR", project_tree["global_inherited"])
|
||||||
|
monkeypatch.setattr(_mod, "GLOBAL_EVOLVED_DIR", project_tree["global_evolved"])
|
||||||
|
monkeypatch.setattr(_mod, "GLOBAL_OBSERVATIONS_FILE", project_tree["homunculus"] / "observations.jsonl")
|
||||||
|
return project_tree
|
||||||
|
|
||||||
|
|
||||||
|
def _make_project(tree, pid="abc123", pname="test-project"):
|
||||||
|
"""Create project directory structure and return a project dict."""
|
||||||
|
project_dir = tree["projects_dir"] / pid
|
||||||
|
personal_dir = project_dir / "instincts" / "personal"
|
||||||
|
inherited_dir = project_dir / "instincts" / "inherited"
|
||||||
|
for d in [personal_dir, inherited_dir,
|
||||||
|
project_dir / "evolved" / "skills",
|
||||||
|
project_dir / "evolved" / "commands",
|
||||||
|
project_dir / "evolved" / "agents",
|
||||||
|
project_dir / "observations.archive"]:
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": pid,
|
||||||
|
"name": pname,
|
||||||
|
"root": str(tree["root"] / "fake-repo"),
|
||||||
|
"remote": "https://github.com/test/test-project.git",
|
||||||
|
"project_dir": project_dir,
|
||||||
|
"instincts_personal": personal_dir,
|
||||||
|
"instincts_inherited": inherited_dir,
|
||||||
|
"evolved_dir": project_dir / "evolved",
|
||||||
|
"observations_file": project_dir / "observations.jsonl",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# parse_instinct_file tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
MULTI_SECTION = """\
|
MULTI_SECTION = """\
|
||||||
---
|
---
|
||||||
@@ -80,3 +216,741 @@ domain: general
|
|||||||
result = parse_instinct_file(content)
|
result = parse_instinct_file(content)
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[0]["content"] == ""
|
assert result[0]["content"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_no_id_skipped():
|
||||||
|
"""Instincts without an 'id' field should be silently dropped."""
|
||||||
|
content = """\
|
||||||
|
---
|
||||||
|
trigger: "when doing nothing"
|
||||||
|
confidence: 0.5
|
||||||
|
---
|
||||||
|
|
||||||
|
No id here.
|
||||||
|
"""
|
||||||
|
result = parse_instinct_file(content)
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_confidence_is_float():
|
||||||
|
content = """\
|
||||||
|
---
|
||||||
|
id: float-check
|
||||||
|
trigger: "when parsing"
|
||||||
|
confidence: 0.42
|
||||||
|
domain: general
|
||||||
|
---
|
||||||
|
|
||||||
|
Body.
|
||||||
|
"""
|
||||||
|
result = parse_instinct_file(content)
|
||||||
|
assert isinstance(result[0]["confidence"], float)
|
||||||
|
assert result[0]["confidence"] == pytest.approx(0.42)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_trigger_strips_quotes():
|
||||||
|
content = """\
|
||||||
|
---
|
||||||
|
id: quote-check
|
||||||
|
trigger: "when quoting"
|
||||||
|
confidence: 0.5
|
||||||
|
domain: general
|
||||||
|
---
|
||||||
|
|
||||||
|
Body.
|
||||||
|
"""
|
||||||
|
result = parse_instinct_file(content)
|
||||||
|
assert result[0]["trigger"] == "when quoting"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_empty_string():
|
||||||
|
result = parse_instinct_file("")
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_garbage_input():
|
||||||
|
result = parse_instinct_file("this is not yaml at all\nno frontmatter here")
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# _validate_file_path tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_validate_normal_path(tmp_path):
|
||||||
|
test_file = tmp_path / "test.yaml"
|
||||||
|
test_file.write_text("hello")
|
||||||
|
result = _validate_file_path(str(test_file), must_exist=True)
|
||||||
|
assert result == test_file.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rejects_etc():
|
||||||
|
with pytest.raises(ValueError, match="system directory"):
|
||||||
|
_validate_file_path("/etc/passwd")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rejects_var_log():
|
||||||
|
with pytest.raises(ValueError, match="system directory"):
|
||||||
|
_validate_file_path("/var/log/syslog")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rejects_usr():
|
||||||
|
with pytest.raises(ValueError, match="system directory"):
|
||||||
|
_validate_file_path("/usr/local/bin/foo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rejects_proc():
|
||||||
|
with pytest.raises(ValueError, match="system directory"):
|
||||||
|
_validate_file_path("/proc/self/status")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_must_exist_fails(tmp_path):
|
||||||
|
with pytest.raises(ValueError, match="does not exist"):
|
||||||
|
_validate_file_path(str(tmp_path / "nonexistent.yaml"), must_exist=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_home_expansion(tmp_path):
|
||||||
|
"""Tilde expansion should work."""
|
||||||
|
result = _validate_file_path("~/test.yaml")
|
||||||
|
assert str(result).startswith(str(Path.home()))
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_relative_path(tmp_path, monkeypatch):
|
||||||
|
"""Relative paths should be resolved."""
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
test_file = tmp_path / "rel.yaml"
|
||||||
|
test_file.write_text("content")
|
||||||
|
result = _validate_file_path("rel.yaml", must_exist=True)
|
||||||
|
assert result == test_file.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# detect_project tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_detect_project_global_fallback(patch_globals, monkeypatch):
|
||||||
|
"""When no git and no env var, should return global project."""
|
||||||
|
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
||||||
|
|
||||||
|
# Mock subprocess.run to simulate git not available
|
||||||
|
def mock_run(*args, **kwargs):
|
||||||
|
raise FileNotFoundError("git not found")
|
||||||
|
|
||||||
|
monkeypatch.setattr("subprocess.run", mock_run)
|
||||||
|
|
||||||
|
project = detect_project()
|
||||||
|
assert project["id"] == "global"
|
||||||
|
assert project["name"] == "global"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_project_from_env(patch_globals, monkeypatch, tmp_path):
|
||||||
|
"""CLAUDE_PROJECT_DIR env var should be used as project root."""
|
||||||
|
fake_repo = tmp_path / "my-repo"
|
||||||
|
fake_repo.mkdir()
|
||||||
|
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
|
||||||
|
|
||||||
|
# Mock git remote to return a URL
|
||||||
|
def mock_run(cmd, **kwargs):
|
||||||
|
if "rev-parse" in cmd:
|
||||||
|
return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
|
||||||
|
if "get-url" in cmd:
|
||||||
|
return SimpleNamespace(returncode=0, stdout="https://github.com/test/my-repo.git\n", stderr="")
|
||||||
|
return SimpleNamespace(returncode=1, stdout="", stderr="")
|
||||||
|
|
||||||
|
monkeypatch.setattr("subprocess.run", mock_run)
|
||||||
|
|
||||||
|
project = detect_project()
|
||||||
|
assert project["id"] != "global"
|
||||||
|
assert project["name"] == "my-repo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_project_git_timeout(patch_globals, monkeypatch):
|
||||||
|
"""Git timeout should fall through to global."""
|
||||||
|
monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
def mock_run(cmd, **kwargs):
|
||||||
|
raise sp.TimeoutExpired(cmd, 5)
|
||||||
|
|
||||||
|
monkeypatch.setattr("subprocess.run", mock_run)
|
||||||
|
|
||||||
|
project = detect_project()
|
||||||
|
assert project["id"] == "global"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_project_creates_directories(patch_globals, monkeypatch, tmp_path):
|
||||||
|
"""detect_project should create the project dir structure."""
|
||||||
|
fake_repo = tmp_path / "structured-repo"
|
||||||
|
fake_repo.mkdir()
|
||||||
|
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(fake_repo))
|
||||||
|
|
||||||
|
def mock_run(cmd, **kwargs):
|
||||||
|
if "rev-parse" in cmd:
|
||||||
|
return SimpleNamespace(returncode=0, stdout=str(fake_repo) + "\n", stderr="")
|
||||||
|
if "get-url" in cmd:
|
||||||
|
return SimpleNamespace(returncode=1, stdout="", stderr="no remote")
|
||||||
|
return SimpleNamespace(returncode=1, stdout="", stderr="")
|
||||||
|
|
||||||
|
monkeypatch.setattr("subprocess.run", mock_run)
|
||||||
|
|
||||||
|
project = detect_project()
|
||||||
|
assert project["instincts_personal"].exists()
|
||||||
|
assert project["instincts_inherited"].exists()
|
||||||
|
assert (project["evolved_dir"] / "skills").exists()
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# _load_instincts_from_dir tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_load_from_empty_dir(tmp_path):
|
||||||
|
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_from_nonexistent_dir(tmp_path):
|
||||||
|
result = _load_instincts_from_dir(tmp_path / "does-not-exist", "personal", "project")
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_annotates_metadata(tmp_path):
|
||||||
|
"""Loaded instincts should have _source_file, _source_type, _scope_label."""
|
||||||
|
yaml_file = tmp_path / "test.yaml"
|
||||||
|
yaml_file.write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["_source_file"] == str(yaml_file)
|
||||||
|
assert result[0]["_source_type"] == "personal"
|
||||||
|
assert result[0]["_scope_label"] == "project"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_defaults_scope_from_label(tmp_path):
|
||||||
|
"""If an instinct has no 'scope' in frontmatter, it should default to scope_label."""
|
||||||
|
no_scope_yaml = """\
|
||||||
|
---
|
||||||
|
id: no-scope
|
||||||
|
trigger: "test"
|
||||||
|
confidence: 0.5
|
||||||
|
domain: general
|
||||||
|
---
|
||||||
|
|
||||||
|
Body.
|
||||||
|
"""
|
||||||
|
(tmp_path / "no-scope.yaml").write_text(no_scope_yaml)
|
||||||
|
result = _load_instincts_from_dir(tmp_path, "inherited", "global")
|
||||||
|
assert result[0]["scope"] == "global"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_preserves_explicit_scope(tmp_path):
|
||||||
|
"""If frontmatter has explicit scope, it should be preserved."""
|
||||||
|
yaml_file = tmp_path / "test.yaml"
|
||||||
|
yaml_file.write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
result = _load_instincts_from_dir(tmp_path, "personal", "global")
|
||||||
|
# Frontmatter says scope: project, scope_label is global
|
||||||
|
# The explicit scope should be preserved (not overwritten)
|
||||||
|
assert result[0]["scope"] == "project"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_handles_corrupt_file(tmp_path, capsys):
|
||||||
|
"""Corrupt YAML files should be warned about but not crash."""
|
||||||
|
# A file that will cause parse_instinct_file to return empty
|
||||||
|
(tmp_path / "good.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
(tmp_path / "bad.yaml").write_text("not yaml\nno frontmatter")
|
||||||
|
|
||||||
|
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
||||||
|
# bad.yaml has no valid instincts (no id), so only good.yaml contributes
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["id"] == "test-instinct"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_supports_yml_extension(tmp_path):
|
||||||
|
yml_file = tmp_path / "test.yml"
|
||||||
|
yml_file.write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
||||||
|
ids = {i["id"] for i in result}
|
||||||
|
assert "test-instinct" in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_supports_md_extension(tmp_path):
|
||||||
|
md_file = tmp_path / "legacy-instinct.md"
|
||||||
|
md_file.write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
result = _load_instincts_from_dir(tmp_path, "personal", "project")
|
||||||
|
ids = {i["id"] for i in result}
|
||||||
|
assert "test-instinct" in ids
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# load_all_instincts tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_load_all_project_and_global(patch_globals):
|
||||||
|
"""Should load from both project and global directories."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
# Write a project instinct
|
||||||
|
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
# Write a global instinct
|
||||||
|
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
||||||
|
|
||||||
|
result = load_all_instincts(project)
|
||||||
|
ids = {i["id"] for i in result}
|
||||||
|
assert "test-instinct" in ids
|
||||||
|
assert "global-instinct" in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_all_project_overrides_global(patch_globals):
|
||||||
|
"""When project and global have same ID, project wins."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
# Same ID but different confidence
|
||||||
|
proj_yaml = SAMPLE_INSTINCT_YAML.replace("id: test-instinct", "id: shared-id")
|
||||||
|
proj_yaml = proj_yaml.replace("confidence: 0.8", "confidence: 0.9")
|
||||||
|
glob_yaml = SAMPLE_GLOBAL_INSTINCT_YAML.replace("id: global-instinct", "id: shared-id")
|
||||||
|
glob_yaml = glob_yaml.replace("confidence: 0.9", "confidence: 0.3")
|
||||||
|
|
||||||
|
(project["instincts_personal"] / "shared.yaml").write_text(proj_yaml)
|
||||||
|
(tree["global_personal"] / "shared.yaml").write_text(glob_yaml)
|
||||||
|
|
||||||
|
result = load_all_instincts(project)
|
||||||
|
shared = [i for i in result if i["id"] == "shared-id"]
|
||||||
|
assert len(shared) == 1
|
||||||
|
assert shared[0]["_scope_label"] == "project"
|
||||||
|
assert shared[0]["confidence"] == 0.9
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_all_global_only(patch_globals):
|
||||||
|
"""Global project should only load global instincts."""
|
||||||
|
tree = patch_globals
|
||||||
|
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
||||||
|
|
||||||
|
global_project = {
|
||||||
|
"id": "global",
|
||||||
|
"name": "global",
|
||||||
|
"root": "",
|
||||||
|
"project_dir": tree["homunculus"],
|
||||||
|
"instincts_personal": tree["global_personal"],
|
||||||
|
"instincts_inherited": tree["global_inherited"],
|
||||||
|
"evolved_dir": tree["global_evolved"],
|
||||||
|
"observations_file": tree["homunculus"] / "observations.jsonl",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = load_all_instincts(global_project)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["id"] == "global-instinct"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_project_only_excludes_global(patch_globals):
|
||||||
|
"""load_project_only_instincts should NOT include global instincts."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
||||||
|
|
||||||
|
result = load_project_only_instincts(project)
|
||||||
|
ids = {i["id"] for i in result}
|
||||||
|
assert "test-instinct" in ids
|
||||||
|
assert "global-instinct" not in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_project_only_global_fallback_loads_global(patch_globals):
|
||||||
|
"""Global fallback should return global instincts for project-only queries."""
|
||||||
|
tree = patch_globals
|
||||||
|
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
||||||
|
|
||||||
|
global_project = {
|
||||||
|
"id": "global",
|
||||||
|
"name": "global",
|
||||||
|
"root": "",
|
||||||
|
"project_dir": tree["homunculus"],
|
||||||
|
"instincts_personal": tree["global_personal"],
|
||||||
|
"instincts_inherited": tree["global_inherited"],
|
||||||
|
"evolved_dir": tree["global_evolved"],
|
||||||
|
"observations_file": tree["homunculus"] / "observations.jsonl",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = load_project_only_instincts(global_project)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["id"] == "global-instinct"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_all_empty(patch_globals):
|
||||||
|
"""No instincts at all should return empty list."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
result = load_all_instincts(project)
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# cmd_status tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_cmd_status_no_instincts(patch_globals, monkeypatch, capsys):
|
||||||
|
"""Status with no instincts should print fallback message."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
||||||
|
|
||||||
|
args = SimpleNamespace()
|
||||||
|
ret = cmd_status(args)
|
||||||
|
assert ret == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "No instincts found." in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_status_with_instincts(patch_globals, monkeypatch, capsys):
|
||||||
|
"""Status should show project and global instinct counts."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
||||||
|
|
||||||
|
(project["instincts_personal"] / "proj.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
(tree["global_personal"] / "glob.yaml").write_text(SAMPLE_GLOBAL_INSTINCT_YAML)
|
||||||
|
|
||||||
|
args = SimpleNamespace()
|
||||||
|
ret = cmd_status(args)
|
||||||
|
assert ret == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "INSTINCT STATUS" in out
|
||||||
|
assert "Project instincts: 1" in out
|
||||||
|
assert "Global instincts: 1" in out
|
||||||
|
assert "PROJECT-SCOPED" in out
|
||||||
|
assert "GLOBAL" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_status_returns_int(patch_globals, monkeypatch):
|
||||||
|
"""cmd_status should always return an int."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
monkeypatch.setattr(_mod, "detect_project", lambda: project)
|
||||||
|
|
||||||
|
args = SimpleNamespace()
|
||||||
|
ret = cmd_status(args)
|
||||||
|
assert isinstance(ret, int)
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# cmd_projects tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_cmd_projects_empty_registry(patch_globals, capsys):
|
||||||
|
"""No projects should print helpful message."""
|
||||||
|
args = SimpleNamespace()
|
||||||
|
ret = cmd_projects(args)
|
||||||
|
assert ret == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "No projects registered yet." in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_projects_with_registry(patch_globals, capsys):
|
||||||
|
"""Should list projects from registry."""
|
||||||
|
tree = patch_globals
|
||||||
|
|
||||||
|
# Create a project dir with instincts
|
||||||
|
pid = "test123abc"
|
||||||
|
project = _make_project(tree, pid=pid, pname="my-app")
|
||||||
|
(project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
# Write registry
|
||||||
|
registry = {
|
||||||
|
pid: {
|
||||||
|
"name": "my-app",
|
||||||
|
"root": "/home/user/my-app",
|
||||||
|
"remote": "https://github.com/user/my-app.git",
|
||||||
|
"last_seen": "2025-01-15T12:00:00Z",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tree["registry_file"].write_text(json.dumps(registry))
|
||||||
|
|
||||||
|
args = SimpleNamespace()
|
||||||
|
ret = cmd_projects(args)
|
||||||
|
assert ret == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "my-app" in out
|
||||||
|
assert pid in out
|
||||||
|
assert "1 personal" in out
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# _promote_specific tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_promote_specific_not_found(patch_globals, capsys):
|
||||||
|
"""Promoting nonexistent instinct should fail."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
ret = _promote_specific(project, "nonexistent", force=True)
|
||||||
|
assert ret == 1
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "not found" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_promote_specific_rejects_invalid_id(patch_globals, capsys):
|
||||||
|
"""Path-like instinct IDs should be rejected before file writes."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
ret = _promote_specific(project, "../escape", force=True)
|
||||||
|
assert ret == 1
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert "Invalid instinct ID" in err
|
||||||
|
|
||||||
|
|
||||||
|
def test_promote_specific_already_global(patch_globals, capsys):
|
||||||
|
"""Promoting an instinct that already exists globally should fail."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
# Write same-id instinct in both project and global
|
||||||
|
(project["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
global_yaml = SAMPLE_INSTINCT_YAML # same id: test-instinct
|
||||||
|
(tree["global_personal"] / "shared.yaml").write_text(global_yaml)
|
||||||
|
|
||||||
|
ret = _promote_specific(project, "test-instinct", force=True)
|
||||||
|
assert ret == 1
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "already exists in global" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_promote_specific_success(patch_globals, capsys):
|
||||||
|
"""Promote a project instinct to global with --force."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
(project["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
ret = _promote_specific(project, "test-instinct", force=True)
|
||||||
|
assert ret == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Promoted" in out
|
||||||
|
|
||||||
|
# Verify file was created in global dir
|
||||||
|
promoted_file = tree["global_personal"] / "test-instinct.yaml"
|
||||||
|
assert promoted_file.exists()
|
||||||
|
content = promoted_file.read_text()
|
||||||
|
assert "scope: global" in content
|
||||||
|
assert "promoted_from: abc123" in content
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# _promote_auto tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_promote_auto_no_candidates(patch_globals, capsys):
|
||||||
|
"""Auto-promote with no cross-project instincts should say so."""
|
||||||
|
tree = patch_globals
|
||||||
|
project = _make_project(tree)
|
||||||
|
|
||||||
|
# Empty registry
|
||||||
|
tree["registry_file"].write_text("{}")
|
||||||
|
|
||||||
|
ret = _promote_auto(project, force=True, dry_run=False)
|
||||||
|
assert ret == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "No instincts qualify" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_promote_auto_dry_run(patch_globals, capsys):
|
||||||
|
"""Dry run should list candidates but not write files."""
|
||||||
|
tree = patch_globals
|
||||||
|
|
||||||
|
# Create two projects with the same high-confidence instinct
|
||||||
|
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
||||||
|
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
||||||
|
|
||||||
|
high_conf_yaml = """\
|
||||||
|
---
|
||||||
|
id: cross-project-instinct
|
||||||
|
trigger: "when reviewing"
|
||||||
|
confidence: 0.95
|
||||||
|
domain: security
|
||||||
|
scope: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Always review for injection.
|
||||||
|
"""
|
||||||
|
(p1["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
|
||||||
|
(p2["instincts_personal"] / "cross.yaml").write_text(high_conf_yaml)
|
||||||
|
|
||||||
|
# Write registry
|
||||||
|
registry = {
|
||||||
|
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
}
|
||||||
|
tree["registry_file"].write_text(json.dumps(registry))
|
||||||
|
|
||||||
|
project = p1
|
||||||
|
ret = _promote_auto(project, force=True, dry_run=True)
|
||||||
|
assert ret == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "DRY RUN" in out
|
||||||
|
assert "cross-project-instinct" in out
|
||||||
|
|
||||||
|
# Verify no file was created
|
||||||
|
assert not (tree["global_personal"] / "cross-project-instinct.yaml").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_promote_auto_writes_file(patch_globals, capsys):
|
||||||
|
"""Auto-promote with force should write global instinct file."""
|
||||||
|
tree = patch_globals
|
||||||
|
|
||||||
|
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
||||||
|
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
||||||
|
|
||||||
|
high_conf_yaml = """\
|
||||||
|
---
|
||||||
|
id: universal-pattern
|
||||||
|
trigger: "when coding"
|
||||||
|
confidence: 0.85
|
||||||
|
domain: general
|
||||||
|
scope: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Use descriptive variable names.
|
||||||
|
"""
|
||||||
|
(p1["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
|
||||||
|
(p2["instincts_personal"] / "uni.yaml").write_text(high_conf_yaml)
|
||||||
|
|
||||||
|
registry = {
|
||||||
|
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
}
|
||||||
|
tree["registry_file"].write_text(json.dumps(registry))
|
||||||
|
|
||||||
|
ret = _promote_auto(p1, force=True, dry_run=False)
|
||||||
|
assert ret == 0
|
||||||
|
|
||||||
|
promoted = tree["global_personal"] / "universal-pattern.yaml"
|
||||||
|
assert promoted.exists()
|
||||||
|
content = promoted.read_text()
|
||||||
|
assert "scope: global" in content
|
||||||
|
assert "auto-promoted" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_promote_auto_skips_invalid_id(patch_globals, capsys):
|
||||||
|
tree = patch_globals
|
||||||
|
|
||||||
|
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
||||||
|
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
||||||
|
|
||||||
|
bad_id_yaml = """\
|
||||||
|
---
|
||||||
|
id: ../escape
|
||||||
|
trigger: "when coding"
|
||||||
|
confidence: 0.9
|
||||||
|
domain: general
|
||||||
|
scope: project
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action
|
||||||
|
Invalid id should be skipped.
|
||||||
|
"""
|
||||||
|
(p1["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
|
||||||
|
(p2["instincts_personal"] / "bad.yaml").write_text(bad_id_yaml)
|
||||||
|
|
||||||
|
registry = {
|
||||||
|
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
}
|
||||||
|
tree["registry_file"].write_text(json.dumps(registry))
|
||||||
|
|
||||||
|
ret = _promote_auto(p1, force=True, dry_run=False)
|
||||||
|
assert ret == 0
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert "Skipping invalid instinct ID" in err
|
||||||
|
assert not (tree["global_personal"] / "../escape.yaml").exists()
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# _find_cross_project_instincts tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_find_cross_project_empty_registry(patch_globals):
|
||||||
|
tree = patch_globals
|
||||||
|
tree["registry_file"].write_text("{}")
|
||||||
|
result = _find_cross_project_instincts()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_cross_project_single_project(patch_globals):
|
||||||
|
"""Single project should return nothing (need 2+)."""
|
||||||
|
tree = patch_globals
|
||||||
|
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
||||||
|
(p1["instincts_personal"] / "inst.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
registry = {"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"}}
|
||||||
|
tree["registry_file"].write_text(json.dumps(registry))
|
||||||
|
|
||||||
|
result = _find_cross_project_instincts()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_cross_project_shared_instinct(patch_globals):
|
||||||
|
"""Same instinct ID in 2 projects should be found."""
|
||||||
|
tree = patch_globals
|
||||||
|
p1 = _make_project(tree, pid="proj1", pname="project-one")
|
||||||
|
p2 = _make_project(tree, pid="proj2", pname="project-two")
|
||||||
|
|
||||||
|
(p1["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
(p2["instincts_personal"] / "shared.yaml").write_text(SAMPLE_INSTINCT_YAML)
|
||||||
|
|
||||||
|
registry = {
|
||||||
|
"proj1": {"name": "project-one", "root": "/a", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
"proj2": {"name": "project-two", "root": "/b", "remote": "", "last_seen": "2025-01-01T00:00:00Z"},
|
||||||
|
}
|
||||||
|
tree["registry_file"].write_text(json.dumps(registry))
|
||||||
|
|
||||||
|
result = _find_cross_project_instincts()
|
||||||
|
assert "test-instinct" in result
|
||||||
|
assert len(result["test-instinct"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# load_registry tests
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_load_registry_missing_file(patch_globals):
|
||||||
|
result = load_registry()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_registry_corrupt_json(patch_globals):
|
||||||
|
tree = patch_globals
|
||||||
|
tree["registry_file"].write_text("not json at all {{{")
|
||||||
|
result = load_registry()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_registry_valid(patch_globals):
|
||||||
|
tree = patch_globals
|
||||||
|
data = {"abc": {"name": "test", "root": "/test"}}
|
||||||
|
tree["registry_file"].write_text(json.dumps(data))
|
||||||
|
result = load_registry()
|
||||||
|
assert result == data
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_instinct_id():
|
||||||
|
assert _validate_instinct_id("good-id_1.0")
|
||||||
|
assert not _validate_instinct_id("../bad")
|
||||||
|
assert not _validate_instinct_id("bad/name")
|
||||||
|
assert not _validate_instinct_id(".hidden")
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_registry_atomic_replaces_file(patch_globals):
|
||||||
|
tree = patch_globals
|
||||||
|
_update_registry("abc123", "demo", "/repo", "https://example.com/repo.git")
|
||||||
|
data = json.loads(tree["registry_file"].read_text())
|
||||||
|
assert "abc123" in data
|
||||||
|
leftovers = list(tree["registry_file"].parent.glob(".projects.json.tmp.*"))
|
||||||
|
assert leftovers == []
|
||||||
|
|||||||
@@ -61,10 +61,11 @@ Use this skill when:
|
|||||||
|
|
||||||
Before writing a utility or adding functionality, mentally run through:
|
Before writing a utility or adding functionality, mentally run through:
|
||||||
|
|
||||||
|
0. Does this already exist in the repo? → `rg` through relevant modules/tests first
|
||||||
1. Is this a common problem? → Search npm/PyPI
|
1. Is this a common problem? → Search npm/PyPI
|
||||||
2. Is there an MCP for this? → Check `~/.claude/settings.json` and search
|
2. Is there an MCP for this? → Check `~/.claude/settings.json` and search
|
||||||
3. Is there a skill for this? → Check `~/.claude/skills/`
|
3. Is there a skill for this? → Check `~/.claude/skills/`
|
||||||
4. Is there a GitHub template? → Search GitHub
|
4. Is there a GitHub implementation/template? → Run GitHub code search for maintained OSS before writing net-new code
|
||||||
|
|
||||||
### Full Mode (agent)
|
### Full Mode (agent)
|
||||||
|
|
||||||
|
|||||||
@@ -1183,7 +1183,7 @@ async function runTests() {
|
|||||||
assert.ok(hooks.hooks.PreCompact, 'Should have PreCompact hooks');
|
assert.ok(hooks.hooks.PreCompact, 'Should have PreCompact hooks');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('all hook commands use node', () => {
|
if (test('all hook commands use node or are skill shell scripts', () => {
|
||||||
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
||||||
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
||||||
|
|
||||||
@@ -1191,9 +1191,14 @@ async function runTests() {
|
|||||||
for (const entry of hookArray) {
|
for (const entry of hookArray) {
|
||||||
for (const hook of entry.hooks) {
|
for (const hook of entry.hooks) {
|
||||||
if (hook.type === 'command') {
|
if (hook.type === 'command') {
|
||||||
|
const isNode = hook.command.startsWith('node');
|
||||||
|
const isSkillScript = hook.command.includes('/skills/') && (
|
||||||
|
/^(bash|sh)\s/.test(hook.command) ||
|
||||||
|
hook.command.startsWith('${CLAUDE_PLUGIN_ROOT}/skills/')
|
||||||
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
hook.command.startsWith('node'),
|
isNode || isSkillScript,
|
||||||
`Hook command should start with 'node': ${hook.command.substring(0, 50)}...`
|
`Hook command should start with 'node' or be a skill shell script: ${hook.command.substring(0, 80)}...`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user