mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-08 18:33:28 +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]
|
||||
# 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
|
||||
github: affaan-m
|
||||
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: Setup Go environment
|
||||
uses: actions/setup-go@v6.2.0
|
||||
with:
|
||||
# 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
|
||||
# Path to the go.mod, go.work, .go-version, or .tool-versions file.
|
||||
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
|
||||
check-latest: # optional
|
||||
# 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.
|
||||
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
|
||||
# Used to specify whether caching is needed. Set to true, if you'd like to enable caching.
|
||||
cache: # optional, default is true
|
||||
# Used to specify the path to a dependency file - go.sum
|
||||
cache-dependency-path: # optional
|
||||
# Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default.
|
||||
architecture: # optional
|
||||
name: Copilot Setup Steps
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
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-export` | Export instincts |
|
||||
| `/evolve` | Cluster instincts into skills |
|
||||
| `/promote` | Promote project instincts to global scope |
|
||||
| `/projects` | List known projects and instinct stats |
|
||||
|
||||
## Available Agents
|
||||
|
||||
|
||||
@@ -95,6 +95,8 @@ opencode
|
||||
| `/instinct-import` | Import instincts |
|
||||
| `/instinct-export` | Export instincts |
|
||||
| `/evolve` | Cluster instincts |
|
||||
| `/promote` | Promote project instincts |
|
||||
| `/projects` | List known projects |
|
||||
|
||||
### Plugin Hooks
|
||||
|
||||
|
||||
@@ -1,112 +1,36 @@
|
||||
---
|
||||
description: Cluster instincts into skills
|
||||
description: Analyze instincts and suggest or generate evolved structures
|
||||
agent: build
|
||||
---
|
||||
|
||||
# Evolve Command
|
||||
|
||||
Cluster related instincts into structured skills: $ARGUMENTS
|
||||
Analyze and evolve instincts in continuous-learning-v2: $ARGUMENTS
|
||||
|
||||
## Your Task
|
||||
|
||||
Analyze instincts and promote clusters to skills.
|
||||
Run:
|
||||
|
||||
## Evolution Process
|
||||
|
||||
### 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)
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" evolve $ARGUMENTS
|
||||
```
|
||||
|
||||
### Step 3: Generate Skill
|
||||
If `CLAUDE_PLUGIN_ROOT` is unavailable, use:
|
||||
|
||||
When cluster has:
|
||||
- 3+ instincts
|
||||
- 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
|
||||
```bash
|
||||
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py evolve $ARGUMENTS
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
Evolution Summary
|
||||
=================
|
||||
|
||||
Clusters Found: X
|
||||
|
||||
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.
|
||||
- Uses project + global instincts for analysis.
|
||||
- Shows skill/command/agent candidates from trigger and domain clustering.
|
||||
- Shows project -> global promotion candidates.
|
||||
- With `--generate`, output path is:
|
||||
- project context: `~/.claude/homunculus/projects/<project-id>/evolved/`
|
||||
- global fallback: `~/.claude/homunculus/evolved/`
|
||||
|
||||
@@ -1,75 +1,29 @@
|
||||
---
|
||||
description: View learned instincts with confidence scores
|
||||
description: Show learned instincts (project + global) with confidence
|
||||
agent: build
|
||||
---
|
||||
|
||||
# Instinct Status Command
|
||||
|
||||
Display learned instincts and their confidence scores: $ARGUMENTS
|
||||
Show instinct status from continuous-learning-v2: $ARGUMENTS
|
||||
|
||||
## Your Task
|
||||
|
||||
Read and display instincts from the continuous-learning-v2 system.
|
||||
Run:
|
||||
|
||||
## Instinct Location
|
||||
|
||||
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)
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" status
|
||||
```
|
||||
|
||||
### Learning Progress
|
||||
If `CLAUDE_PLUGIN_ROOT` is unavailable, use:
|
||||
|
||||
- Total instincts: X
|
||||
- This session: X
|
||||
- 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"
|
||||
}
|
||||
```bash
|
||||
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status
|
||||
```
|
||||
|
||||
## Confidence Calculation
|
||||
## Behavior Notes
|
||||
|
||||
```
|
||||
confidence = (successes + 1) / (applications + 2)
|
||||
```
|
||||
|
||||
Bayesian smoothing ensures new instincts don't have extreme confidence.
|
||||
|
||||
---
|
||||
|
||||
**TIP**: Use `/evolve` to cluster related instincts into skills when confidence is high.
|
||||
- Output includes both project-scoped and global instincts.
|
||||
- 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.
|
||||
|
||||
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": {
|
||||
"description": "Cluster instincts into skills",
|
||||
"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": {
|
||||
|
||||
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)
|
||||
|
||||
### 🔬 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
|
||||
|
||||
The instinct-based learning system automatically learns your patterns:
|
||||
@@ -557,8 +563,15 @@ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
# Copy commands
|
||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||
|
||||
# Copy skills
|
||||
cp -r everything-claude-code/skills/* ~/.claude/skills/
|
||||
# Copy skills (core vs niche)
|
||||
# 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
|
||||
@@ -972,6 +985,8 @@ OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ event t
|
||||
| `/instinct-import` | Import instincts |
|
||||
| `/instinct-export` | Export instincts |
|
||||
| `/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 |
|
||||
| `/setup-pm` | Configure package manager |
|
||||
|
||||
|
||||
@@ -287,6 +287,8 @@ everything-claude-code/
|
||||
/instinct-import <file> # 从他人导入直觉
|
||||
/instinct-export # 导出你的直觉以供分享
|
||||
/evolve # 将相关直觉聚类到技能中
|
||||
/promote # 将项目级直觉提升为全局直觉
|
||||
/projects # 查看已识别项目与直觉统计
|
||||
```
|
||||
|
||||
完整文档见 `skills/continuous-learning-v2/`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: evolve
|
||||
description: Cluster related instincts into skills, commands, or agents
|
||||
description: Analyze instincts and suggest or generate evolved structures
|
||||
command: true
|
||||
---
|
||||
|
||||
@@ -29,9 +29,7 @@ Analyzes instincts and clusters related ones into higher-level structures:
|
||||
|
||||
```
|
||||
/evolve # Analyze all instincts and suggest evolutions
|
||||
/evolve --domain testing # Only evolve instincts in testing domain
|
||||
/evolve --dry-run # Show what would be created without creating
|
||||
/evolve --threshold 5 # Require 5+ related instincts to cluster
|
||||
/evolve --generate # Also generate files under evolved/{skills,commands,agents}
|
||||
```
|
||||
|
||||
## Evolution Rules
|
||||
@@ -78,63 +76,50 @@ Example:
|
||||
|
||||
## What to Do
|
||||
|
||||
1. Read all instincts from `~/.claude/homunculus/instincts/`
|
||||
2. Group instincts by:
|
||||
- Domain similarity
|
||||
- Trigger pattern overlap
|
||||
- Action sequence relationship
|
||||
3. For each cluster of 3+ related instincts:
|
||||
- Determine evolution type (command/skill/agent)
|
||||
- Generate the appropriate file
|
||||
- Save to `~/.claude/homunculus/evolved/{commands,skills,agents}/`
|
||||
4. Link evolved structure back to source instincts
|
||||
1. Detect current project context
|
||||
2. Read project + global instincts (project takes precedence on ID conflicts)
|
||||
3. Group instincts by trigger/domain patterns
|
||||
4. Identify:
|
||||
- Skill candidates (trigger clusters with 2+ instincts)
|
||||
- Command candidates (high-confidence workflow instincts)
|
||||
- Agent candidates (larger, high-confidence clusters)
|
||||
5. Show promotion candidates (project -> global) when applicable
|
||||
6. If `--generate` is passed, write files to:
|
||||
- Project scope: `~/.claude/homunculus/projects/<project-id>/evolved/`
|
||||
- Global fallback: `~/.claude/homunculus/evolved/`
|
||||
|
||||
## 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
|
||||
Instincts: new-table-migration, update-schema, regenerate-types
|
||||
Type: Command
|
||||
Confidence: 85% (based on 12 observations)
|
||||
## SKILL CANDIDATES
|
||||
1. Cluster: "adding tests"
|
||||
Instincts: 3
|
||||
Avg confidence: 82%
|
||||
Domains: testing
|
||||
Scopes: project
|
||||
|
||||
Would create: /new-table command
|
||||
Files:
|
||||
- ~/.claude/homunculus/evolved/commands/new-table.md
|
||||
## COMMAND CANDIDATES (2)
|
||||
/adding-tests
|
||||
From: test-first-workflow [project]
|
||||
Confidence: 84%
|
||||
|
||||
## Cluster 2: Functional Code Style
|
||||
Instincts: prefer-functional, use-immutable, avoid-classes, pure-functions
|
||||
Type: Skill
|
||||
Confidence: 78% (based on 8 observations)
|
||||
|
||||
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.
|
||||
## AGENT CANDIDATES (1)
|
||||
adding-tests-agent
|
||||
Covers 3 instincts
|
||||
Avg confidence: 82%
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
- `--execute`: Actually create the evolved structures (default is preview)
|
||||
- `--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
|
||||
- `--generate`: Generate evolved files in addition to analysis output
|
||||
|
||||
## Generated File Format
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
@@ -18,17 +18,18 @@ Exports instincts to a shareable format. Perfect for:
|
||||
/instinct-export --domain testing # Export only testing instincts
|
||||
/instinct-export --min-confidence 0.7 # Only export high-confidence instincts
|
||||
/instinct-export --output team-instincts.yaml
|
||||
/instinct-export --scope project --output project-instincts.yaml
|
||||
```
|
||||
|
||||
## What to Do
|
||||
|
||||
1. Read instincts from `~/.claude/homunculus/instincts/personal/`
|
||||
2. Filter based on flags
|
||||
3. Strip sensitive information:
|
||||
- Remove session IDs
|
||||
- Remove file paths (keep only patterns)
|
||||
- Remove timestamps older than "last week"
|
||||
4. Generate export file
|
||||
1. Detect current project context
|
||||
2. Load instincts by selected scope:
|
||||
- `project`: current project only
|
||||
- `global`: global only
|
||||
- `all`: project + global merged (default)
|
||||
3. Apply filters (`--domain`, `--min-confidence`)
|
||||
4. Write YAML-style export to file (or stdout if no output path provided)
|
||||
|
||||
## Output Format
|
||||
|
||||
@@ -40,52 +41,26 @@ Creates a YAML file:
|
||||
# Source: personal
|
||||
# Count: 12 instincts
|
||||
|
||||
version: "2.0"
|
||||
exported_by: "continuous-learning-v2"
|
||||
export_date: "2025-01-22T10:30:00Z"
|
||||
---
|
||||
id: prefer-functional-style
|
||||
trigger: "when writing new functions"
|
||||
confidence: 0.8
|
||||
domain: code-style
|
||||
source: session-observation
|
||||
scope: project
|
||||
project_id: a1b2c3d4e5f6
|
||||
project_name: my-app
|
||||
---
|
||||
|
||||
instincts:
|
||||
- id: prefer-functional-style
|
||||
trigger: "when writing new functions"
|
||||
action: "Use functional patterns over classes"
|
||||
confidence: 0.8
|
||||
domain: code-style
|
||||
observations: 8
|
||||
# Prefer Functional Style
|
||||
|
||||
- id: test-first-workflow
|
||||
trigger: "when adding new functionality"
|
||||
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
|
||||
## Action
|
||||
Use functional patterns over classes.
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
- `--domain <name>`: Export only specified domain
|
||||
- `--min-confidence <n>`: Minimum confidence threshold (default: 0.3)
|
||||
- `--output <file>`: Output file path (default: instincts-export-YYYYMMDD.yaml)
|
||||
- `--format <yaml|json|md>`: Output format (default: yaml)
|
||||
- `--include-evidence`: Include evidence text (default: excluded)
|
||||
- `--min-confidence <n>`: Minimum confidence threshold
|
||||
- `--output <file>`: Output file path (prints to stdout when omitted)
|
||||
- `--scope <project|global|all>`: Export scope (default: `all`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
@@ -11,7 +11,7 @@ command: true
|
||||
Run the instinct CLI using the plugin root path:
|
||||
|
||||
```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):
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
Import instincts from:
|
||||
- Teammates' exports
|
||||
- Skill Creator (repo analysis)
|
||||
- Community collections
|
||||
- Previous machine backups
|
||||
Import instincts from local file paths or HTTP(S) URLs.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/instinct-import team-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
|
||||
@@ -40,7 +37,9 @@ Import instincts from:
|
||||
2. Parse and validate the format
|
||||
3. Check for duplicates with existing 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
|
||||
|
||||
@@ -71,60 +70,33 @@ Already have similar instincts:
|
||||
Import: 0.9 confidence
|
||||
→ Update to import (higher confidence)
|
||||
|
||||
## Conflicting Instincts (1)
|
||||
These contradict local instincts:
|
||||
❌ use-classes-for-services
|
||||
Conflicts with: avoid-classes
|
||||
→ Skip (requires manual resolution)
|
||||
|
||||
---
|
||||
Import 8 new, update 1, skip 3?
|
||||
Import 8 new, update 1?
|
||||
```
|
||||
|
||||
## Merge Strategies
|
||||
## Merge Behavior
|
||||
|
||||
### For Duplicates
|
||||
When importing an instinct that matches an existing one:
|
||||
- **Higher confidence wins**: Keep the one with higher confidence
|
||||
- **Merge evidence**: Combine observation counts
|
||||
- **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
|
||||
When importing an instinct with an existing ID:
|
||||
- Higher-confidence import becomes an update candidate
|
||||
- Equal/lower-confidence import is skipped
|
||||
- User confirms unless `--force` is used
|
||||
|
||||
## Source Tracking
|
||||
|
||||
Imported instincts are marked with:
|
||||
```yaml
|
||||
source: "inherited"
|
||||
source: inherited
|
||||
scope: project
|
||||
imported_from: "team-instincts.yaml"
|
||||
imported_at: "2025-01-22T10:30:00Z"
|
||||
original_source: "session-observation" # or "repo-analysis"
|
||||
project_id: "a1b2c3d4e5f6"
|
||||
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
|
||||
|
||||
- `--dry-run`: Preview without importing
|
||||
- `--force`: Import even if conflicts exist
|
||||
- `--merge-strategy <higher|local|import>`: How to handle duplicates
|
||||
- `--from-skill-creator <owner/repo>`: Import from Skill Creator analysis
|
||||
- `--force`: Skip confirmation prompt
|
||||
- `--min-confidence <n>`: Only import instincts above threshold
|
||||
- `--scope <project|global>`: Select target scope (default: `project`)
|
||||
|
||||
## Output
|
||||
|
||||
@@ -134,7 +106,7 @@ After import:
|
||||
|
||||
Added: 8 instincts
|
||||
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/
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
name: instinct-status
|
||||
description: Show all learned instincts with their confidence levels
|
||||
description: Show learned instincts (project + global) with confidence
|
||||
command: true
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
@@ -26,61 +26,34 @@ python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status
|
||||
|
||||
```
|
||||
/instinct-status
|
||||
/instinct-status --domain code-style
|
||||
/instinct-status --low-confidence
|
||||
```
|
||||
|
||||
## What to Do
|
||||
|
||||
1. Read all instinct files from `~/.claude/homunculus/instincts/personal/`
|
||||
2. Read inherited instincts from `~/.claude/homunculus/instincts/inherited/`
|
||||
3. Display them grouped by domain with confidence bars
|
||||
1. Detect current project context (git remote/path hash)
|
||||
2. Read project instincts from `~/.claude/homunculus/projects/<project-id>/instincts/`
|
||||
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
|
||||
|
||||
```
|
||||
📊 Instinct Status
|
||||
==================
|
||||
============================================================
|
||||
INSTINCT STATUS - 12 total
|
||||
============================================================
|
||||
|
||||
## Code Style (4 instincts)
|
||||
Project: my-app (a1b2c3d4e5f6)
|
||||
Project instincts: 8
|
||||
Global instincts: 4
|
||||
|
||||
### prefer-functional-style
|
||||
Trigger: when writing new functions
|
||||
Action: Use functional patterns over classes
|
||||
Confidence: ████████░░ 80%
|
||||
Source: session-observation | Last updated: 2025-01-22
|
||||
## PROJECT-SCOPED (my-app)
|
||||
### WORKFLOW (3)
|
||||
███████░░░ 70% grep-before-edit [project]
|
||||
trigger: when modifying code
|
||||
|
||||
### use-path-aliases
|
||||
Trigger: when importing modules
|
||||
Action: Use @/ path aliases instead of relative imports
|
||||
Confidence: ██████░░░░ 60%
|
||||
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)
|
||||
## GLOBAL (apply to all projects)
|
||||
### SECURITY (2)
|
||||
█████████░ 85% validate-user-input [global]
|
||||
trigger: when handling user input
|
||||
```
|
||||
|
||||
## 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": [
|
||||
{
|
||||
"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)"
|
||||
@@ -51,6 +51,18 @@
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
@@ -129,6 +141,18 @@
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
|
||||
@@ -66,6 +66,14 @@
|
||||
"url": "https://mcp.clickhouse.cloud/mcp",
|
||||
"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": {
|
||||
"command": "npx",
|
||||
"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.
|
||||
|
||||
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
|
||||
|
||||
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**
|
||||
- Use **planner** agent to create implementation plan
|
||||
- Generate planning docs before coding: PRD, architecture, system_design, tech_doc, task_list
|
||||
- Identify dependencies and risks
|
||||
- Break down into phases
|
||||
|
||||
|
||||
@@ -64,7 +64,23 @@ mkdir -p $TARGET/skills $TARGET/rules
|
||||
|
||||
## 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`:
|
||||
|
||||
@@ -77,7 +93,7 @@ Options:
|
||||
- "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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
### 2c: Execute Installation
|
||||
### 2d: Execute Installation
|
||||
|
||||
For each selected skill, copy the entire skill directory:
|
||||
```bash
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
---
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -18,8 +19,21 @@ Inspired in part by the Homunculus work from [humanplane](https://github.com/hum
|
||||
- Tuning confidence thresholds for learned behaviors
|
||||
- Reviewing, exporting, or importing instinct libraries
|
||||
- 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 |
|
||||
|---------|----|----|
|
||||
@@ -27,7 +41,7 @@ Inspired in part by the Homunculus work from [humanplane](https://github.com/hum
|
||||
| Analysis | Main context | Background agent (Haiku) |
|
||||
| Granularity | Full skills | Atomic "instincts" |
|
||||
| Confidence | None | 0.3-0.9 weighted |
|
||||
| Evolution | Direct to skill | Instincts → cluster → skill/command/agent |
|
||||
| Evolution | Direct to skill | Instincts -> cluster -> skill/command/agent |
|
||||
| Sharing | None | Export/import instincts |
|
||||
|
||||
## The Instinct Model
|
||||
@@ -41,6 +55,9 @@ trigger: "when writing new functions"
|
||||
confidence: 0.7
|
||||
domain: "code-style"
|
||||
source: "session-observation"
|
||||
scope: project
|
||||
project_id: "a1b2c3d4e5f6"
|
||||
project_name: "my-react-app"
|
||||
---
|
||||
|
||||
# Prefer Functional Style
|
||||
@@ -54,51 +71,69 @@ Use functional patterns over classes when appropriate.
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- **Atomic** — one trigger, one action
|
||||
- **Confidence-weighted** — 0.3 = tentative, 0.9 = near certain
|
||||
- **Domain-tagged** — code-style, testing, git, debugging, workflow, etc.
|
||||
- **Evidence-backed** — tracks what observations created it
|
||||
- **Atomic** -- one trigger, one action
|
||||
- **Confidence-weighted** -- 0.3 = tentative, 0.9 = near certain
|
||||
- **Domain-tagged** -- code-style, testing, git, debugging, workflow, etc.
|
||||
- **Evidence-backed** -- tracks what observations created it
|
||||
- **Scope-aware** -- `project` (default) or `global`
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Session Activity
|
||||
│
|
||||
│ Hooks capture prompts + tool use (100% reliable)
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ observations.jsonl │
|
||||
│ (prompts, tool calls, outcomes) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ Observer agent reads (background, Haiku)
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ PATTERN DETECTION │
|
||||
│ • User corrections → instinct │
|
||||
│ • Error resolutions → instinct │
|
||||
│ • Repeated workflows → instinct │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ Creates/updates
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ instincts/personal/ │
|
||||
│ • prefer-functional.md (0.7) │
|
||||
│ • always-test-first.md (0.9) │
|
||||
│ • use-zod-validation.md (0.6) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ /evolve clusters
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ evolved/ │
|
||||
│ • commands/new-feature.md │
|
||||
│ • skills/testing-workflow.md │
|
||||
│ • agents/refactor-specialist.md │
|
||||
└─────────────────────────────────────────┘
|
||||
Session Activity (in a git repo)
|
||||
|
|
||||
| Hooks capture prompts + tool use (100% reliable)
|
||||
| + detect project context (git remote / repo path)
|
||||
v
|
||||
+---------------------------------------------+
|
||||
| projects/<project-hash>/observations.jsonl |
|
||||
| (prompts, tool calls, outcomes, project) |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
| Observer agent reads (background, Haiku)
|
||||
v
|
||||
+---------------------------------------------+
|
||||
| PATTERN DETECTION |
|
||||
| * User corrections -> instinct |
|
||||
| * Error resolutions -> instinct |
|
||||
| * Repeated workflows -> instinct |
|
||||
| * Scope decision: project or global? |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
| Creates/updates
|
||||
v
|
||||
+---------------------------------------------+
|
||||
| projects/<project-hash>/instincts/personal/ |
|
||||
| * prefer-functional.yaml (0.7) [project] |
|
||||
| * use-react-hooks.yaml (0.9) [project] |
|
||||
+---------------------------------------------+
|
||||
| instincts/personal/ (GLOBAL) |
|
||||
| * always-validate-input.yaml (0.85) [global]|
|
||||
| * grep-before-edit.yaml (0.6) [global] |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
| /evolve clusters + /promote
|
||||
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
|
||||
|
||||
### 1. Enable Observation Hooks
|
||||
@@ -114,14 +149,14 @@ Add to your `~/.claude/settings.json`.
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"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": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"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": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh pre"
|
||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh"
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"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
|
||||
|
||||
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
|
||||
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands}}
|
||||
touch ~/.claude/homunculus/observations.jsonl
|
||||
# Global directories
|
||||
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
|
||||
|
||||
```bash
|
||||
/instinct-status # Show learned instincts with confidence scores
|
||||
/instinct-status # Show learned instincts (project + global)
|
||||
/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
|
||||
/promote # Promote project instincts to global scope
|
||||
/projects # List all known projects and their instinct counts
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/instinct-status` | Show all learned instincts with confidence |
|
||||
| `/evolve` | Cluster related instincts into skills/commands |
|
||||
| `/instinct-export` | Export instincts for sharing |
|
||||
| `/instinct-import <file>` | Import instincts from others |
|
||||
| `/instinct-status` | Show all instincts (project-scoped + global) with confidence |
|
||||
| `/evolve` | Cluster related instincts into skills/commands, suggest promotions |
|
||||
| `/instinct-export` | Export instincts (filterable by scope/domain) |
|
||||
| `/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
|
||||
|
||||
Edit `config.json`:
|
||||
Edit `config.json` to control the background observer:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"observation": {
|
||||
"enabled": true,
|
||||
"store_path": "~/.claude/homunculus/observations.jsonl",
|
||||
"max_file_size_mb": 10,
|
||||
"archive_after_days": 7
|
||||
},
|
||||
"instincts": {
|
||||
"personal_path": "~/.claude/homunculus/instincts/personal/",
|
||||
"inherited_path": "~/.claude/homunculus/instincts/inherited/",
|
||||
"min_confidence": 0.3,
|
||||
"auto_approve_threshold": 0.7,
|
||||
"confidence_decay_rate": 0.05
|
||||
},
|
||||
"version": "2.1",
|
||||
"observer": {
|
||||
"enabled": true,
|
||||
"model": "haiku",
|
||||
"enabled": false,
|
||||
"run_interval_minutes": 5,
|
||||
"patterns_to_detect": [
|
||||
"user_corrections",
|
||||
"error_resolutions",
|
||||
"repeated_workflows",
|
||||
"tool_preferences"
|
||||
]
|
||||
},
|
||||
"evolution": {
|
||||
"cluster_threshold": 3,
|
||||
"evolved_path": "~/.claude/homunculus/evolved/"
|
||||
"min_observations_to_analyze": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 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
|
||||
|
||||
```
|
||||
~/.claude/homunculus/
|
||||
├── identity.json # Your profile, technical level
|
||||
├── observations.jsonl # Current session observations
|
||||
├── observations.archive/ # Processed observations
|
||||
├── instincts/
|
||||
│ ├── personal/ # Auto-learned instincts
|
||||
│ └── inherited/ # Imported from others
|
||||
└── evolved/
|
||||
├── agents/ # Generated specialist agents
|
||||
├── skills/ # Generated skills
|
||||
└── commands/ # Generated commands
|
||||
+-- identity.json # Your profile, technical level
|
||||
+-- projects.json # Registry: project hash -> name/path/remote
|
||||
+-- observations.jsonl # Global observations (fallback)
|
||||
+-- instincts/
|
||||
| +-- personal/ # Global auto-learned instincts
|
||||
| +-- inherited/ # Global imported instincts
|
||||
+-- evolved/
|
||||
| +-- agents/ # Global generated agents
|
||||
| +-- skills/ # Global generated skills
|
||||
| +-- 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**:
|
||||
- Traditional SKILL.md files (for backward compatibility)
|
||||
- Instinct collections (for v2 learning system)
|
||||
| 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** | "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
|
||||
|
||||
@@ -263,7 +330,7 @@ Confidence evolves over time:
|
||||
|
||||
## 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:
|
||||
- Every tool call is observed
|
||||
@@ -272,17 +339,19 @@ Hooks fire **100% of the time**, deterministically. This means:
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
v2 is fully compatible with v1:
|
||||
- Existing `~/.claude/skills/learned/` skills still work
|
||||
v2.1 is fully compatible with v2.0 and v1:
|
||||
- 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)
|
||||
- Gradual migration path: run both in parallel
|
||||
- Gradual migration: run both in parallel
|
||||
|
||||
## Privacy
|
||||
|
||||
- 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
|
||||
- You control what gets exported
|
||||
- You control what gets exported and promoted
|
||||
|
||||
## 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
|
||||
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
|
||||
run_mode: background
|
||||
---
|
||||
|
||||
# Observer Agent
|
||||
@@ -11,20 +10,21 @@ A background agent that analyzes observations from Claude Code sessions to detec
|
||||
|
||||
## When to Run
|
||||
|
||||
- After significant session activity (20+ tool calls)
|
||||
- When user runs `/analyze-patterns`
|
||||
- After enough observations accumulate (configurable, default 20)
|
||||
- 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
|
||||
|
||||
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
|
||||
{"timestamp":"2025-01-22T10:30:00Z","event":"tool_start","session":"abc123","tool":"Edit","input":"..."}
|
||||
{"timestamp":"2025-01-22T10:30:01Z","event":"tool_complete","session":"abc123","tool":"Edit","output":"..."}
|
||||
{"timestamp":"2025-01-22T10:30:05Z","event":"tool_start","session":"abc123","tool":"Bash","input":"npm test"}
|
||||
{"timestamp":"2025-01-22T10:30:10Z","event":"tool_complete","session":"abc123","tool":"Bash","output":"All tests pass"}
|
||||
{"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":"...","project_id":"a1b2c3d4e5f6","project_name":"my-react-app"}
|
||||
{"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","project_id":"a1b2c3d4e5f6","project_name":"my-react-app"}
|
||||
```
|
||||
|
||||
## Pattern Detection
|
||||
@@ -65,28 +65,75 @@ When certain tools are consistently preferred:
|
||||
|
||||
## 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
|
||||
---
|
||||
id: prefer-grep-before-edit
|
||||
trigger: "when searching for code to modify"
|
||||
id: use-react-hooks-pattern
|
||||
trigger: "when creating React components"
|
||||
confidence: 0.65
|
||||
domain: "workflow"
|
||||
domain: "code-style"
|
||||
source: "session-observation"
|
||||
scope: project
|
||||
project_id: "a1b2c3d4e5f6"
|
||||
project_name: "my-react-app"
|
||||
---
|
||||
|
||||
# Prefer Grep Before Edit
|
||||
# Use React Hooks Pattern
|
||||
|
||||
## Action
|
||||
Always use Grep to find the exact location before using Edit.
|
||||
Always use functional components with hooks instead of class components.
|
||||
|
||||
## Evidence
|
||||
- Observed 8 times in session abc123
|
||||
- Pattern: Grep → Read → Edit sequence
|
||||
- Pattern: All new components use useState/useEffect
|
||||
- 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
|
||||
|
||||
Initial confidence based on observation frequency:
|
||||
@@ -100,6 +147,15 @@ Confidence adjusts over time:
|
||||
- -0.1 for each contradicting observation
|
||||
- -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
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
Given observations:
|
||||
```jsonl
|
||||
{"event":"tool_start","tool":"Grep","input":"pattern: useState"}
|
||||
{"event":"tool_complete","tool":"Grep","output":"Found in 3 files"}
|
||||
{"event":"tool_start","tool":"Read","input":"src/hooks/useAuth.ts"}
|
||||
{"event":"tool_complete","tool":"Read","output":"[file content]"}
|
||||
{"event":"tool_start","tool":"Edit","input":"src/hooks/useAuth.ts..."}
|
||||
{"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","project_id":"a1b2c3","project_name":"my-app"}
|
||||
{"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]","project_id":"a1b2c3","project_name":"my-app"}
|
||||
{"event":"tool_start","tool":"Edit","input":"src/hooks/useAuth.ts...","project_id":"a1b2c3","project_name":"my-app"}
|
||||
```
|
||||
|
||||
Analysis:
|
||||
- Detected workflow: Grep → Read → Edit
|
||||
- Frequency: Seen 5 times this session
|
||||
- **Scope decision**: This is a general workflow pattern (not project-specific) → **global**
|
||||
- Create instinct:
|
||||
- trigger: "when modifying code"
|
||||
- action: "Search with Grep, confirm with Read, then Edit"
|
||||
- confidence: 0.6
|
||||
- domain: "workflow"
|
||||
- scope: "global"
|
||||
|
||||
## Integration with Skill Creator
|
||||
|
||||
When instincts are imported from Skill Creator (repo analysis), they have:
|
||||
- `source: "repo-analysis"`
|
||||
- `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+).
|
||||
|
||||
@@ -4,26 +4,79 @@
|
||||
# Starts the background observer agent that analyzes observations
|
||||
# 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:
|
||||
# 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 status # Check if observer is running
|
||||
|
||||
set -e
|
||||
|
||||
CONFIG_DIR="${HOME}/.claude/homunculus"
|
||||
PID_FILE="${CONFIG_DIR}/.observer.pid"
|
||||
LOG_FILE="${CONFIG_DIR}/observer.log"
|
||||
OBSERVATIONS_FILE="${CONFIG_DIR}/observations.jsonl"
|
||||
# ─────────────────────────────────────────────
|
||||
# Project detection
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
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
|
||||
stop)
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
pid=$(cat "$PID_FILE")
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "Stopping observer (PID: $pid)..."
|
||||
echo "Stopping observer for ${PROJECT_NAME} (PID: $pid)..."
|
||||
kill "$pid"
|
||||
rm -f "$PID_FILE"
|
||||
echo "Observer stopped."
|
||||
@@ -44,6 +97,9 @@ case "${1:-start}" in
|
||||
echo "Observer is running (PID: $pid)"
|
||||
echo "Log: $LOG_FILE"
|
||||
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
|
||||
else
|
||||
echo "Observer not running (stale PID file)"
|
||||
@@ -57,17 +113,24 @@ case "${1:-start}" in
|
||||
;;
|
||||
|
||||
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
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
pid=$(cat "$PID_FILE")
|
||||
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
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
echo "Starting observer agent..."
|
||||
echo "Starting observer agent for ${PROJECT_NAME}..."
|
||||
|
||||
# The observer loop
|
||||
(
|
||||
@@ -79,18 +142,43 @@ case "${1:-start}" in
|
||||
return
|
||||
fi
|
||||
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
|
||||
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
|
||||
# This spawns a quick analysis session
|
||||
# The prompt now specifies project-scoped instinct creation
|
||||
if command -v claude &> /dev/null; then
|
||||
exit_code=0
|
||||
claude --model haiku --max-turns 3 --print \
|
||||
"Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \
|
||||
claude --model haiku --print \
|
||||
"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=$?
|
||||
if [ "$exit_code" -ne 0 ]; then
|
||||
echo "[$(date)] Claude analysis failed (exit $exit_code)" >> "$LOG_FILE"
|
||||
@@ -101,10 +189,9 @@ case "${1:-start}" in
|
||||
|
||||
# Archive processed observations
|
||||
if [ -f "$OBSERVATIONS_FILE" ]; then
|
||||
archive_dir="${CONFIG_DIR}/observations.archive"
|
||||
archive_dir="${PROJECT_DIR}/observations.archive"
|
||||
mkdir -p "$archive_dir"
|
||||
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S).jsonl" 2>/dev/null || true
|
||||
touch "$OBSERVATIONS_FILE"
|
||||
mv "$OBSERVATIONS_FILE" "$archive_dir/processed-$(date +%Y%m%d-%H%M%S)-$$.jsonl" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -112,11 +199,11 @@ case "${1:-start}" in
|
||||
trap 'analyze_observations' USR1
|
||||
|
||||
echo "$$" > "$PID_FILE"
|
||||
echo "[$(date)] Observer started (PID: $$)" >> "$LOG_FILE"
|
||||
echo "[$(date)] Observer started for ${PROJECT_NAME} (PID: $$)" >> "$LOG_FILE"
|
||||
|
||||
while true; do
|
||||
# Check every 5 minutes
|
||||
sleep 300
|
||||
# Check at configured interval (default: 5 minutes)
|
||||
sleep "$OBSERVER_INTERVAL_SECONDS"
|
||||
|
||||
analyze_observations
|
||||
done
|
||||
|
||||
@@ -1,41 +1,8 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"observation": {
|
||||
"enabled": true,
|
||||
"store_path": "~/.claude/homunculus/observations.jsonl",
|
||||
"max_file_size_mb": 10,
|
||||
"archive_after_days": 7,
|
||||
"capture_tools": ["Edit", "Write", "Bash", "Read", "Grep", "Glob"],
|
||||
"ignore_tools": ["TodoWrite"]
|
||||
},
|
||||
"instincts": {
|
||||
"personal_path": "~/.claude/homunculus/instincts/personal/",
|
||||
"inherited_path": "~/.claude/homunculus/instincts/inherited/",
|
||||
"min_confidence": 0.3,
|
||||
"auto_approve_threshold": 0.7,
|
||||
"confidence_decay_rate": 0.02,
|
||||
"max_instincts": 100
|
||||
},
|
||||
"version": "2.1",
|
||||
"observer": {
|
||||
"enabled": false,
|
||||
"model": "haiku",
|
||||
"run_interval_minutes": 5,
|
||||
"min_observations_to_analyze": 20,
|
||||
"patterns_to_detect": [
|
||||
"user_corrections",
|
||||
"error_resolutions",
|
||||
"repeated_workflows",
|
||||
"tool_preferences",
|
||||
"file_patterns"
|
||||
]
|
||||
},
|
||||
"evolution": {
|
||||
"cluster_threshold": 3,
|
||||
"evolved_path": "~/.claude/homunculus/evolved/",
|
||||
"auto_evolve": false
|
||||
},
|
||||
"integration": {
|
||||
"skill_creator_api": "https://skill-creator.app/api",
|
||||
"backward_compatible_v1": true
|
||||
"min_observations_to_analyze": 20
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,52 +4,20 @@
|
||||
# Captures tool use events for pattern analysis.
|
||||
# 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}:
|
||||
# {
|
||||
# "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" }]
|
||||
# }]
|
||||
# }
|
||||
# }
|
||||
# Registered via plugin hooks/hooks.json (auto-loaded when plugin is enabled).
|
||||
# Can also be registered manually in ~/.claude/settings.json.
|
||||
|
||||
set -e
|
||||
|
||||
# Hook phase from CLI argument: "pre" (PreToolUse) or "post" (PostToolUse)
|
||||
HOOK_PHASE="${1:-post}"
|
||||
|
||||
CONFIG_DIR="${HOME}/.claude/homunculus"
|
||||
OBSERVATIONS_FILE="${CONFIG_DIR}/observations.jsonl"
|
||||
MAX_FILE_SIZE_MB=10
|
||||
|
||||
# Ensure directory exists
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# Skip if disabled
|
||||
if [ -f "$CONFIG_DIR/disabled" ]; then
|
||||
exit 0
|
||||
fi
|
||||
# ─────────────────────────────────────────────
|
||||
# Read stdin first (before project detection)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
# Read JSON from stdin (Claude Code hook format)
|
||||
INPUT_JSON=$(cat)
|
||||
@@ -59,6 +27,51 @@ if [ -z "$INPUT_JSON" ]; then
|
||||
exit 0
|
||||
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)
|
||||
# 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 '
|
||||
@@ -80,6 +93,8 @@ try:
|
||||
tool_input = data.get("tool_input", data.get("input", {}))
|
||||
tool_output = data.get("tool_output", data.get("output", ""))
|
||||
session_id = data.get("session_id", "unknown")
|
||||
tool_use_id = data.get("tool_use_id", "")
|
||||
cwd = data.get("cwd", "")
|
||||
|
||||
# Truncate large inputs/outputs
|
||||
if isinstance(tool_input, dict):
|
||||
@@ -88,24 +103,26 @@ try:
|
||||
tool_input_str = str(tool_input)[:5000]
|
||||
|
||||
if isinstance(tool_output, dict):
|
||||
tool_output_str = json.dumps(tool_output)[:5000]
|
||||
tool_response_str = json.dumps(tool_output)[:5000]
|
||||
else:
|
||||
tool_output_str = str(tool_output)[:5000]
|
||||
tool_response_str = str(tool_output)[:5000]
|
||||
|
||||
print(json.dumps({
|
||||
"parsed": True,
|
||||
"event": event,
|
||||
"tool": tool_name,
|
||||
"input": tool_input_str if event == "tool_start" else None,
|
||||
"output": tool_output_str if event == "tool_complete" else None,
|
||||
"session": session_id
|
||||
"output": tool_response_str if event == "tool_complete" else None,
|
||||
"session": session_id,
|
||||
"tool_use_id": tool_use_id,
|
||||
"cwd": cwd
|
||||
}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"parsed": False, "error": str(e)}))
|
||||
')
|
||||
|
||||
# Check if parsing succeeded
|
||||
PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.stdin).get('parsed', False))")
|
||||
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
|
||||
# Fallback: log raw input for debugging
|
||||
@@ -119,20 +136,23 @@ print(json.dumps({'timestamp': os.environ['TIMESTAMP'], 'event': 'parse_error',
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Archive if file too large
|
||||
# Archive if file too large (atomic: rename with unique suffix to avoid race)
|
||||
if [ -f "$OBSERVATIONS_FILE" ]; then
|
||||
file_size_mb=$(du -m "$OBSERVATIONS_FILE" 2>/dev/null | cut -f1)
|
||||
if [ "${file_size_mb:-0}" -ge "$MAX_FILE_SIZE_MB" ]; then
|
||||
archive_dir="${CONFIG_DIR}/observations.archive"
|
||||
archive_dir="${PROJECT_DIR}/observations.archive"
|
||||
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
|
||||
|
||||
# Build and write observation
|
||||
# Build and write observation (now includes project context)
|
||||
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
export PROJECT_ID_ENV="$PROJECT_ID"
|
||||
export PROJECT_NAME_ENV="$PROJECT_NAME"
|
||||
export TIMESTAMP="$timestamp"
|
||||
|
||||
echo "$PARSED" | python3 -c "
|
||||
import json, sys, os
|
||||
|
||||
@@ -141,10 +161,12 @@ observation = {
|
||||
'timestamp': os.environ['TIMESTAMP'],
|
||||
'event': parsed['event'],
|
||||
'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']
|
||||
if parsed['output'] is not None:
|
||||
observation['output'] = parsed['output']
|
||||
@@ -152,13 +174,14 @@ if parsed['output'] is not None:
|
||||
print(json.dumps(observation))
|
||||
" >> "$OBSERVATIONS_FILE"
|
||||
|
||||
# Signal observer if running
|
||||
OBSERVER_PID_FILE="${CONFIG_DIR}/.observer.pid"
|
||||
if [ -f "$OBSERVER_PID_FILE" ]; then
|
||||
observer_pid=$(cat "$OBSERVER_PID_FILE")
|
||||
if kill -0 "$observer_pid" 2>/dev/null; then
|
||||
kill -USR1 "$observer_pid" 2>/dev/null || true
|
||||
# Signal observer if running (check both project-scoped and global observer)
|
||||
for pid_file in "${PROJECT_DIR}/.observer.pid" "${CONFIG_DIR}/.observer.pid"; do
|
||||
if [ -f "$pid_file" ]; then
|
||||
observer_pid=$(cat "$pid_file")
|
||||
if kill -0 "$observer_pid" 2>/dev/null; then
|
||||
kill -USR1 "$observer_pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
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 json
|
||||
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)
|
||||
_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)
|
||||
_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 = """\
|
||||
---
|
||||
@@ -80,3 +216,741 @@ domain: general
|
||||
result = parse_instinct_file(content)
|
||||
assert len(result) == 1
|
||||
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:
|
||||
|
||||
0. Does this already exist in the repo? → `rg` through relevant modules/tests first
|
||||
1. Is this a common problem? → Search npm/PyPI
|
||||
2. Is there an MCP for this? → Check `~/.claude/settings.json` and search
|
||||
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)
|
||||
|
||||
|
||||
@@ -1183,7 +1183,7 @@ async function runTests() {
|
||||
assert.ok(hooks.hooks.PreCompact, 'Should have PreCompact hooks');
|
||||
})) 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 hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
||||
|
||||
@@ -1191,9 +1191,14 @@ async function runTests() {
|
||||
for (const entry of hookArray) {
|
||||
for (const hook of entry.hooks) {
|
||||
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(
|
||||
hook.command.startsWith('node'),
|
||||
`Hook command should start with 'node': ${hook.command.substring(0, 50)}...`
|
||||
isNode || isSkillScript,
|
||||
`Hook command should start with 'node' or be a skill shell script: ${hook.command.substring(0, 80)}...`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user