diff --git a/.gitignore b/.gitignore index 19210d9a..090cef80 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__ kaizen_data demo/workdir/.claude/ .claude +dist diff --git a/CLI.md b/CLI.md index 3494bcf3..cdc34094 100644 --- a/CLI.md +++ b/CLI.md @@ -62,6 +62,38 @@ kaizen entities show my_namespace 12345 kaizen entities delete my_namespace 12345 ``` +### Skill Management + +```bash +# Package all skills from default location (plugins/kaizen/skills → dist/) +kaizen skills package + +# Preview what would be packaged (no files created) +kaizen skills package --dry-run + +# Package from a custom source directory +kaizen skills package --source ./my-skills + +# Package to a custom output directory +kaizen skills package --output ./dist + +# Remove existing .skill files before packaging +kaizen skills package --clean + +# Combine options +kaizen skills package --source ./my-skills --output ./dist --clean +``` + +**Options:** +- `--source, -s`: Source directory containing skill folders (default: `plugins/kaizen/skills`) +- `--output, -o`: Output directory for `.skill` files (default: `dist`) +- `--clean`: Remove existing `.skill` files in output directory before packaging +- `--dry-run`: Show what would be packaged without creating files + +**Skill Requirements:** +- Each skill must be a directory containing a `SKILL.md` file +- The resulting `.skill` file is a ZIP archive with the skill directory as the top-level folder + ## Examples ```bash @@ -84,6 +116,10 @@ uv run kaizen entities search coding_guidelines "error handling" # List all guidelines uv run kaizen entities list coding_guidelines --type guideline + +# Package skills for distribution +uv run kaizen skills package --dry-run # Preview first +uv run kaizen skills package --clean # Package with clean output ``` ## Environment Variables diff --git a/kaizen/cli/cli.py b/kaizen/cli/cli.py index 10b3b199..91dad297 100644 --- a/kaizen/cli/cli.py +++ b/kaizen/cli/cli.py @@ -1,6 +1,9 @@ """Kaizen CLI for managing entities and namespaces.""" import json +import sys +import zipfile +from pathlib import Path from typing import Annotated, Optional import typer @@ -19,10 +22,12 @@ namespaces_app = typer.Typer(help="Namespace management commands") entities_app = typer.Typer(help="Entity management commands") sync_app = typer.Typer(help="Sync commands") +skills_app = typer.Typer(help="Skill management commands") app.add_typer(namespaces_app, name="namespaces") app.add_typer(entities_app, name="entities") app.add_typer(sync_app, name="sync") +app.add_typer(skills_app, name="skills") console = Console() @@ -374,5 +379,99 @@ def sync_phoenix( raise typer.Exit(1) +# ============================================================================= +# Skills Commands +# ============================================================================= + + +@skills_app.command("package") +def package_skills( + source: Annotated[Path, typer.Option("--source", "-s", help="Source skills directory")] = Path("plugins/kaizen/skills"), + output: Annotated[Path, typer.Option("--output", "-o", help="Output directory for .skill files")] = Path("dist"), + clean: Annotated[bool, typer.Option("--clean", help="Remove existing .skill files before packaging")] = False, + dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be packaged without creating files")] = False, +): + """Package plugin skills into .skill files for distribution.""" + # Validate source directory + if not source.exists(): + console.print(f"[red]Source directory not found: {source}[/red]") + raise typer.Exit(1) + + if not source.is_dir(): + console.print(f"[red]Source is not a directory: {source}[/red]") + raise typer.Exit(1) + + # Find valid skill directories (those containing SKILL.md) + skill_dirs: list[tuple[str, Path]] = [] + for item in sorted(source.iterdir()): + if item.is_dir(): + skill_md = item / "SKILL.md" + if skill_md.exists(): + skill_dirs.append((item.name, item)) + + if not skill_dirs: + console.print(f"[yellow]No skills found in {source}[/yellow]") + console.print("[dim]Skills must contain a SKILL.md file[/dim]") + raise typer.Exit(0) + + # Display found skills + console.print(f"[bold]Found {len(skill_dirs)} skill(s) in {source}[/bold]\n") + + table = Table(title="Skills to Package") + table.add_column("Skill", style="cyan") + table.add_column("Files", justify="right") + table.add_column("Output", style="dim") + + for skill_name, skill_path in skill_dirs: + file_count = sum(1 for _ in skill_path.rglob("*") if _.is_file()) + output_file = output / f"{skill_name}.skill" + table.add_row(skill_name, str(file_count), str(output_file)) + + console.print(table) + console.print() + + if dry_run: + console.print("[yellow]Dry run - no files created[/yellow]") + return + + # Create output directory if needed + output.mkdir(parents=True, exist_ok=True) + + # Clean existing .skill files if requested + if clean: + existing_skills = list(output.glob("*.skill")) + if existing_skills: + console.print(f"[dim]Removing {len(existing_skills)} existing .skill file(s)...[/dim]") + for skill_file in existing_skills: + skill_file.unlink() + + # Package each skill + packaged = 0 + failed = 0 + for skill_name, skill_path in skill_dirs: + output_file = output / f"{skill_name}.skill" + + try: + with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + for file_path in skill_path.rglob("*"): + if file_path.is_file(): + # Archive path includes skill name as top-level directory + arcname = f"{skill_name}/{file_path.relative_to(skill_path)}" + zf.write(file_path, arcname) + + console.print(f"[green]Packaged:[/green] {skill_name} -> {output_file}") + packaged += 1 + + except (OSError, PermissionError, zipfile.LargeZipFile, zipfile.BadZipFile, ValueError) as e: + console.print(f"[red]Failed to package {skill_name}: {e}[/red]") + failed += 1 + + if failed == 0: + console.print(f"\n[bold green]Successfully packaged {packaged}/{len(skill_dirs)} skill(s)[/bold green]") + else: + console.print(f"\n[bold yellow]Packaged {packaged}/{len(skill_dirs)} skill(s); {failed} failed[/bold yellow]") + sys.exit(1) + + if __name__ == "__main__": app() diff --git a/plugins/kaizen/.claude-plugin/plugin.json b/plugins/kaizen/.claude-plugin/plugin.json index 5be3ae58..37d8f37e 100644 --- a/plugins/kaizen/.claude-plugin/plugin.json +++ b/plugins/kaizen/.claude-plugin/plugin.json @@ -1,9 +1,9 @@ { "name": "kaizen", "version": "1.0.0", - "description": "Learn from conversations with auto-generated guidelines", + "description": "Learn from conversations with auto-generated entities", "author": { "name": "Vinod Muthusamy" }, "skills": "./skills/" -} \ No newline at end of file +} diff --git a/plugins/kaizen/README.md b/plugins/kaizen/README.md index 64b2da74..8d849b32 100644 --- a/plugins/kaizen/README.md +++ b/plugins/kaizen/README.md @@ -1,11 +1,11 @@ -# Guidelines Plugin for Claude Code +# Kaizen Plugin for Claude Code -A plugin that helps Claude Code learn from conversations by automatically extracting and applying guidelines. +A plugin that helps Claude Code learn from conversations by automatically extracting and applying entities. ## Features -- **Automatic Retrieval**: At the start of each prompt, relevant guidelines are automatically injected -- **Manual Learning**: Use the `/kaizen:learn` skill to extract and save guidelines from conversations +- **Automatic Retrieval**: At the start of each prompt, relevant entities are automatically injected +- **Manual Learning**: Use the `/kaizen:learn` skill to extract and save entities from conversations - **Zero-config Retrieval**: Hooks are automatically installed when the plugin is enabled ## Installation @@ -27,34 +27,34 @@ claude --plugin-dir /path/to/kaizen/repo/plugins/kaizen ## How It Works -### Guideline Retrieval (Automatic) +### Entity Retrieval (Automatic) When you submit a prompt, the plugin automatically: -1. Loads all stored guidelines from `.claude/guidelines.json` +1. Loads all stored entities from `.claude/entities.json` 2. Formats and injects them into the conversation context -3. Claude applies relevant guidelines to the current task +3. Claude applies relevant entities to the current task -### Guideline Generation (Manual by Default) +### Entity Generation (Manual by Default) -By default, you must manually invoke the `/kaizen:learn` skill to extract guidelines: +By default, you must manually invoke the `/kaizen:learn` skill to extract entities: 1. Complete a conversation or task -2. Invoke `/kaizen:learn` +2. Invoke `/kaizen:learn` 3. The plugin analyzes the conversation trajectory -4. Extracts actionable guidelines from what worked/failed -5. Saves new guidelines to `.claude/guidelines.json` +4. Extracts actionable entities from what worked/failed +5. Saves new entities to `.claude/entities.json` ## Skills Included ### `/kaizen:learn` -Manually invoke to extract guidelines from the current conversation: +Manually invoke to extract entities from the current conversation: - Analyzes task, steps taken, successes and failures -- Generates proactive guidelines (what to do, not what to avoid) +- Generates proactive entities (what to do, not what to avoid) - Outputs JSON for storage ### `/kaizen:recall` -Manually invoke to retrieve and display stored guidelines. +Manually invoke to retrieve and display stored entities. ### `/kaizen:save` @@ -72,15 +72,13 @@ Assistant: "What would you like to name this skill?" User: "my-workflow-name" ``` -See [SAVE_SKILL.md](SAVE_SKILL.md) for detailed documentation. +## Entities Storage -## Guidelines Storage - -Guidelines are stored in `.claude/guidelines.json`: +Entities are stored in `.claude/entities.json`: ```json { - "guidelines": [ + "entities": [ { "content": "Use Python PIL/Pillow for image metadata extraction in sandboxed environments", "rationale": "System tools like exiftool may not be available", @@ -93,8 +91,8 @@ Guidelines are stored in `.claude/guidelines.json`: ## Environment Variables -- `GUIDELINES_FILE`: Override the default guidelines storage location -- `CLAUDE_PROJECT_ROOT`: Set by Claude Code, used to locate project-level guidelines +- `KAIZEN_ENTITIES_FILE`: Override the default entities storage location +- `CLAUDE_PROJECT_ROOT`: Set by Claude Code, used to locate project-level entities ## Verification @@ -108,16 +106,17 @@ kaizen/ │ └── plugin.json # Plugin manifest ├── skills/ │ ├── learn/ -│ │ └── SKILL.md +│ │ ├── SKILL.md +│ │ └── scripts/ +│ │ └── save_entities.py │ ├── recall/ -│ │ └── SKILL.md +│ │ ├── SKILL.md +│ │ └── scripts/ +│ │ └── retrieve_entities.py │ └── save/ │ └── SKILL.md ├── hooks/ │ └── hooks.json # Auto-configured hooks -├── scripts/ -│ ├── save_guidelines.py -│ └── retrieve_guidelines.py └── README.md ``` diff --git a/plugins/kaizen/SAVE_SKILL.md b/plugins/kaizen/SAVE_SKILL.md deleted file mode 100644 index dfcdabed..00000000 --- a/plugins/kaizen/SAVE_SKILL.md +++ /dev/null @@ -1,291 +0,0 @@ -# Save Skill Documentation - -## Overview - -The **save** skill is a powerful tool that captures successful workflows from your current session and transforms them into reusable skills. It analyzes your conversation history, identifies patterns, and generates comprehensive documentation along with helper scripts. - -## Location - -The save skill is part of the Kaizen plugin: -``` -/plugins/kaizen/skills/save/SKILL.md -``` - -## What It Does - -When you invoke the `save` skill after completing a successful task, it: - -1. **Analyzes your session** - Reviews user requests, reasoning steps, tool calls, and responses -2. **Identifies patterns** - Extracts the workflow sequence and decision points -3. **Parameterizes values** - Converts session-specific values into reusable parameters -4. **Generates documentation** - Creates a comprehensive SKILL.md file -5. **Creates helper scripts** - Generates Python scripts for programmatic operations (when applicable) -6. **Saves everything** - Stores the skill in `~/.claude/skills/{skill-name}/` - -## When to Use - -Use the save skill when you: -- Complete a multi-step task successfully -- Discover a useful workflow pattern -- Want to standardize a process for future use -- Solve a problem that might recur -- Work through a complex workflow that involves data processing, file operations, or API calls - -## How to Use - -### Basic Usage - -After completing a successful task: - -``` -User: "save" -``` - -The skill will: -1. Analyze your current session -2. Ask you for a skill name -3. Generate SKILL.md and any helper scripts -4. Save to `~/.claude/skills/{skill-name}/` -5. Provide a detailed summary - -### Example Session - -``` -User: "Read the config.json file and parse it" -Assistant: [Successfully reads and parses the file] -User: "Great! save" -Assistant: "What would you like to name this skill?" -User: "read-and-parse-config" -Assistant: [Generates skill and scripts] -``` - -## Generated Output - -### Directory Structure - -``` -~/.claude/skills/{skill-name}/ -├── SKILL.md # Comprehensive documentation -└── scripts/ # Helper scripts (if applicable) - ├── parse_data.py - └── validate_data.py -``` - -### SKILL.md Contents - -The generated SKILL.md includes: -- **Overview**: What the skill does and when to use it -- **Parameters**: Required inputs with descriptions -- **Workflow**: Step-by-step instructions -- **Helper Scripts**: Documentation for any generated scripts -- **Error Handling**: Common errors and solutions -- **Examples**: Real-world usage examples -- **Notes**: Additional tips and context - -### Helper Scripts - -Scripts are automatically generated when your workflow includes: -- Data transformation or parsing (JSON, CSV, XML) -- File operations (reading, writing, searching) -- API calls or HTTP requests -- Complex calculations or data analysis -- Repetitive operations that could be automated - -## Parameterization - -The save skill uses **conservative parameterization**, meaning it only parameterizes obvious session-specific values: - -**Parameterized**: -- File paths: `/home/user/project/file.txt` → `{project_dir}/{filename}` -- Specific names: `myapp` → `{app_name}` -- User data: `john@example.com` → `{email}` - -**Kept Unchanged**: -- Tool names: `read_file`, `execute_command` -- General patterns and logic -- Error handling approaches -- Workflow structure - -## Examples - -### Example 1: File Reading Workflow - -**Session**: -``` -User: "Read states.txt using the filesystem MCP tool" -Assistant: [Handles permission errors, searches for file, reads successfully] -User: "save" -``` - -**Generated Skill**: `read-file-with-permission-check` - -**Files Created**: -``` -~/.claude/skills/read-file-with-permission-check/ -├── SKILL.md -└── scripts/ - └── search_and_read.py -``` - -### Example 2: API Integration Workflow - -**Session**: -``` -User: "Fetch user data from the API and format it" -Assistant: [Makes API call, processes response, formats output] -User: "save" -``` - -**Generated Skill**: `fetch-and-format-user-data` - -**Files Created**: -``` -~/.claude/skills/fetch-and-format-user-data/ -├── SKILL.md -└── scripts/ - ├── api_client.py - └── format_data.py -``` - -### Example 3: Simple Tool Call (No Scripts) - -**Session**: -``` -User: "List all Python files in the project" -Assistant: [Uses glob tool to find *.py files] -User: "save" -``` - -**Generated Skill**: `list-python-files` - -**Files Created**: -``` -~/.claude/skills/list-python-files/ -└── SKILL.md -``` - -## Skill Naming Guidelines - -When prompted for a skill name, follow these guidelines: - -- **Use lowercase letters**: `my-skill` not `My-Skill` -- **Separate words with hyphens**: `read-and-parse` not `read_and_parse` -- **Be descriptive**: `deploy-to-staging` not `deploy` -- **Keep it concise**: `analyze-logs` not `analyze-application-logs-for-errors` - -**Good Examples**: -- `read-file-with-permissions` -- `deploy-to-staging` -- `analyze-logs` -- `fetch-user-data` - -**Bad Examples**: -- `My Skill!` (spaces and special characters) -- `skill` (too generic) -- `read_file` (underscores instead of hyphens) - -## Handling Conflicts - -If a skill with the chosen name already exists, you'll be prompted to: -- **Overwrite** the existing skill -- **Choose a different name** -- **Cancel** the operation - -## Tips for Better Skills - -1. **Complete the task first**: Ensure your workflow is successful before saving -2. **Clear session**: The clearer your workflow, the better the generated skill -3. **Descriptive names**: Choose names that clearly indicate what the skill does -4. **Test the skill**: After saving, test it in a new session to verify it works -5. **Refine manually**: Edit the generated SKILL.md and scripts to add more context or examples - -## Advanced Usage - -### Manual Editing - -After generation, you can manually edit: -- **SKILL.md**: Add more examples, refine descriptions, update parameters -- **Scripts**: Add error handling, optimize performance, add features - -### Skill Composition - -Generated skills can reference other skills: - -```markdown -## Workflow - -### Step 1: Fetch Data -Use the `fetch-user-data` skill to retrieve user information. - -### Step 2: Process Data -Use the `parse-json-data` skill to parse the response. -``` - -### Version Control - -Consider adding your `~/.claude/skills/` directory to version control: - -```bash -cd ~/.claude/skills -git init -git add . -git commit -m "Initial skills collection" -``` - -## Troubleshooting - -### Session Too Short - -**Problem**: "Session has fewer than 3 meaningful exchanges" - -**Solution**: Complete more of the task before invoking the save skill - -### No Clear Workflow - -**Problem**: "Conversation doesn't show a clear workflow pattern" - -**Solution**: Describe the key steps you want to capture when prompted - -### Invalid Skill Name - -**Problem**: "Skill name contains invalid characters" - -**Solution**: Use lowercase letters and hyphens only (e.g., `my-skill`) - -### Script Generation Errors - -**Problem**: Script generation fails - -**Solution**: The SKILL.md will still be saved. You can add scripts manually later. - -## Technical Details - -### Session Context - -The save skill accesses the current session context, which includes: -- User utterances -- Agent reasoning (thinking tags) -- Tool calls with arguments -- Tool responses -- Agent responses - -No external fetching or Phoenix integration is required - the context is automatically available when the skill is invoked. - -### Storage Location - -Skills are saved to `~/.claude/skills/` in your home directory, making them available across all projects. - -### Script Templates - -Generated scripts follow a standard template with: -- Proper argument parsing -- Error handling -- JSON output (when appropriate) -- Usage documentation -- Executable permissions - -## Support - -For issues or questions about the save skill: -1. Review the generated SKILL.md for specific workflow questions -2. Manually edit generated skills to customize them for your needs diff --git a/plugins/kaizen/hooks/hooks.json b/plugins/kaizen/hooks/hooks.json index 33f4198c..7187615e 100644 --- a/plugins/kaizen/hooks/hooks.json +++ b/plugins/kaizen/hooks/hooks.json @@ -6,10 +6,10 @@ "hooks": [ { "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/retrieve_guidelines.py" + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/skills/recall/scripts/retrieve_entities.py" } ] } ] } -} \ No newline at end of file +} diff --git a/plugins/kaizen/scripts/retrieve_guidelines.py b/plugins/kaizen/scripts/retrieve_guidelines.py deleted file mode 100755 index e8b4b412..00000000 --- a/plugins/kaizen/scripts/retrieve_guidelines.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -"""Retrieve and output guidelines for Claude to filter.""" - -import json -import os -import sys -from pathlib import Path -import datetime - -# Debug logging -LOG_FILE = "/tmp/guidelines-plugin.log" - -def log(message): - """Append a timestamped message to the log file.""" - timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - with open(LOG_FILE, "a") as f: - f.write(f"[{timestamp}] [retrieve] {message}\n") - -log("Script started") - -# Log all environment variables -log("=== Environment Variables ===") -for key, value in sorted(os.environ.items()): - # Mask sensitive values - if any(sensitive in key.upper() for sensitive in ['PASSWORD', 'SECRET', 'TOKEN', 'KEY', 'API']): - log(f" {key}=***MASKED***") - else: - log(f" {key}={value}") -log("=== End Environment Variables ===") - -# Log command-line arguments -log("=== Command-Line Arguments ===") -log(f" sys.argv: {sys.argv}") -log(f" Script path: {sys.argv[0] if sys.argv else 'N/A'}") -log(f" Arguments: {sys.argv[1:] if len(sys.argv) > 1 else 'None'}") -log("=== End Command-Line Arguments ===") - - -def find_guidelines_file(): - """Find the guidelines file in common locations.""" - locations = [ - os.environ.get("GUIDELINES_FILE"), - # Project root from Claude Code - os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/guidelines.json"), - # Current working directory - ".claude/guidelines.json", - # Plugin-relative path (fallback) - str(Path(__file__).parent.parent / "guidelines.json"), - ] - for loc in locations: - if loc and Path(loc).exists(): - return Path(loc) - return None - - -def load_guidelines(): - """Load guidelines from the guidelines file.""" - guidelines_file = find_guidelines_file() - if not guidelines_file: - return [] - try: - with open(guidelines_file) as f: - data = json.load(f) - return data.get("guidelines", []) - except (json.JSONDecodeError, IOError): - return [] - - -def format_guidelines(guidelines): - """Format all guidelines for Claude to review.""" - header = """## Guidelines for this task - -Review these guidelines and apply any relevant ones: - -""" - items = [] - for g in guidelines: - content = g.get('content') - if not content: - continue - item = f"- **[{g.get('category', 'general')}]** {content}" - if g.get('rationale'): - item += f"\n - _Rationale: {g['rationale']}_" - if g.get('trigger'): - item += f"\n - _When: {g['trigger']}_" - items.append(item) - - return header + "\n".join(items) - - -def main(): - # Read input from stdin (hook provides JSON with prompt) - try: - input_data = json.load(sys.stdin) - log("=== Input Data ===") - log(f" Keys: {list(input_data.keys())}") - log(f" Full content: {json.dumps(input_data, indent=2)}") - log("=== End Input Data ===") - except json.JSONDecodeError as e: - log(f"Failed to parse JSON input: {e}") - return - - # Load all guidelines - guidelines_file = find_guidelines_file() - log(f"Guidelines file: {guidelines_file}") - - guidelines = load_guidelines() - if not guidelines: - log("No guidelines found") - return - - log(f"Loaded {len(guidelines)} guidelines") - - # Output all guidelines - Claude will filter for relevance - output = format_guidelines(guidelines) - print(output) - log(f"Output {len(output)} chars to stdout") - - -if __name__ == "__main__": - main() diff --git a/plugins/kaizen/scripts/save_guidelines.py b/plugins/kaizen/scripts/save_guidelines.py deleted file mode 100755 index df0ecc56..00000000 --- a/plugins/kaizen/scripts/save_guidelines.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -""" -Save Guidelines Script -Reads guidelines from stdin and appends them to the guidelines file. -""" - -import json -import os -import sys -from pathlib import Path -import datetime - -# Debug logging -LOG_FILE = "/tmp/guidelines-plugin.log" - -def log(message): - """Append a timestamped message to the log file.""" - timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - with open(LOG_FILE, "a") as f: - f.write(f"[{timestamp}] [save] {message}\n") - -log("Script started") - - -def find_guidelines_file(): - """Find existing guidelines file, checking multiple locations.""" - locations = [ - os.environ.get("GUIDELINES_FILE"), - # Project root from Claude Code - os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/guidelines.json"), - # Current working directory - ".claude/guidelines.json", - # Plugin-relative path (fallback) - str(Path(__file__).parent.parent / "guidelines.json"), - ] - - for loc in locations: - if loc and Path(loc).exists(): - return Path(loc).resolve() - - return None - - -def get_default_guidelines_path(): - """Get default path for new guidelines file.""" - # Prefer project root if available - project_root = os.environ.get("CLAUDE_PROJECT_ROOT", "") - if project_root: - claude_dir = Path(project_root) / ".claude" - else: - # Fall back to current directory's .claude/ - claude_dir = Path(".claude") - - claude_dir.mkdir(parents=True, exist_ok=True) - return (claude_dir / "guidelines.json").resolve() - - -def load_existing_guidelines(path): - """Load existing guidelines from file.""" - try: - with open(path) as f: - data = json.load(f) - return data.get("guidelines", []) - except (json.JSONDecodeError, FileNotFoundError): - return [] - - -def save_guidelines(path, guidelines): - """Save guidelines to file.""" - path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "w") as f: - json.dump({"guidelines": guidelines}, f, indent=2) - f.write("\n") - - -def main(): - # Read guidelines from stdin - try: - input_data = json.load(sys.stdin) - log(f"Received input with keys: {list(input_data.keys())}") - except json.JSONDecodeError as e: - log(f"Failed to parse JSON input: {e}") - print(f"Error: Invalid JSON input - {e}", file=sys.stderr) - sys.exit(1) - - new_guidelines = input_data.get("guidelines", []) - if not new_guidelines: - log("No guidelines in input") - print("No guidelines provided in input.", file=sys.stderr) - sys.exit(0) - - log(f"Received {len(new_guidelines)} new guidelines") - - # Find or create guidelines file - existing_path = find_guidelines_file() - - if existing_path: - guidelines_path = existing_path - existing_guidelines = load_existing_guidelines(guidelines_path) - log(f"Found existing file: {guidelines_path} with {len(existing_guidelines)} guidelines") - print(f"Appending to existing file: {guidelines_path}") - else: - guidelines_path = get_default_guidelines_path() - existing_guidelines = [] - log(f"Creating new file: {guidelines_path}") - print(f"Creating new file: {guidelines_path}") - - # Merge guidelines (avoid duplicates by content) - existing_contents = {g.get("content") for g in existing_guidelines if g.get("content")} - added_count = 0 - - for guideline in new_guidelines: - content = guideline.get("content") - if not content: - log(f"Skipping guideline without content: {guideline}") - continue - if content not in existing_contents: - existing_guidelines.append(guideline) - existing_contents.add(content) - added_count += 1 - - # Save merged guidelines - save_guidelines(guidelines_path, existing_guidelines) - - log(f"Added {added_count} new guidelines. Total: {len(existing_guidelines)}") - print(f"Added {added_count} new guideline(s). Total: {len(existing_guidelines)}") - print(f"Guidelines stored in: {guidelines_path}") - - -if __name__ == "__main__": - main() diff --git a/plugins/kaizen/skills/learn/SKILL.md b/plugins/kaizen/skills/learn/SKILL.md index b1f1a605..5117b3bd 100644 --- a/plugins/kaizen/skills/learn/SKILL.md +++ b/plugins/kaizen/skills/learn/SKILL.md @@ -1,13 +1,13 @@ --- name: learn -description: Extract actionable guidelines from conversation trajectories. Analyzes user requests, steps taken, successes and failures to generate proactive guidelines that help on similar future tasks. +description: Extract actionable entities from conversation trajectories. Analyzes user requests, steps taken, successes and failures to generate proactive entities that help on similar future tasks. --- -# Guideline Generator +# Entity Generator ## Overview -This skill analyzes conversation trajectories to extract actionable guidelines that would help on similar tasks in the future. It transforms reactive learnings (what failed) into proactive recommendations (what to do first). +This skill analyzes conversation trajectories to extract actionable entities that would help on similar tasks in the future. It transforms reactive learnings (what failed) into proactive recommendations (what to do first). ## Workflow @@ -20,9 +20,9 @@ Identify from your current conversation: - **What Worked**: Which approaches succeeded? - **What Failed**: Which approaches didn't work and why? -### Step 2: Extract Guidelines +### Step 2: Extract Entities -Extract 3-5 proactive guidelines following these principles: +Extract 3-5 proactive entities following these principles: 1. **Reframe failures as proactive recommendations:** - If an approach failed due to permissions → recommend the alternative FIRST @@ -37,15 +37,15 @@ Extract 3-5 proactive guidelines following these principles: - Bad trigger: "When apt-get fails" - Good trigger: "When working in containerized/sandboxed environments" -### Step 3: Output Guidelines JSON +### Step 3: Output Entities JSON -Output guidelines in the following JSON format: +Output entities in the following JSON format: ```json { - "guidelines": [ + "entities": [ { - "content": "Proactive guideline stating what TO DO", + "content": "Proactive entity stating what TO DO", "rationale": "Why this approach works better", "category": "strategy|recovery|optimization", "trigger": "Situational context when this applies" @@ -54,35 +54,38 @@ Output guidelines in the following JSON format: } ``` -### Step 4: Save Guidelines +### Step 4: Save Entities -After generating the guidelines JSON, save them using the save_guidelines.py script: +After generating the entities JSON, save them using the save_entities.py script: + +#### Method 1: Direct Pipe (Recommended) -**Method 1: Direct Pipe (Recommended)** ```bash -echo '' | python3 ${CLAUDE_PLUGIN_ROOT}/scripts/save_guidelines.py +echo '' | python3 ${CLAUDE_PLUGIN_ROOT}/skills/learn/scripts/save_entities.py ``` -**Method 2: From File** +#### Method 2: From File + ```bash -cat guidelines.json | python3 ${CLAUDE_PLUGIN_ROOT}/scripts/save_guidelines.py +cat entities.json | python3 ${CLAUDE_PLUGIN_ROOT}/skills/learn/scripts/save_entities.py ``` -**Method 3: Interactive** +#### Method 3: Interactive + ```bash -python3 ${CLAUDE_PLUGIN_ROOT}/scripts/save_guidelines.py +python3 ${CLAUDE_PLUGIN_ROOT}/skills/learn/scripts/save_entities.py # Then paste your JSON and press Ctrl+D ``` The script will: -- Find or create the guidelines file (`.claude/guidelines.json`) -- Merge new guidelines with existing ones (avoiding duplicates) +- Find or create the entities file (`.claude/entities.json`) +- Merge new entities with existing ones (avoiding duplicates) - Display confirmation with the total count **Example:** ```bash echo '{ - "guidelines": [ + "entities": [ { "content": "Use Python PIL/Pillow for image metadata extraction", "rationale": "System tools may not be available in sandboxed environments", @@ -90,19 +93,19 @@ echo '{ "trigger": "When extracting image metadata in containerized environments" } ] -}' | python3 ${CLAUDE_PLUGIN_ROOT}/scripts/save_guidelines.py +}' | python3 ${CLAUDE_PLUGIN_ROOT}/skills/learn/scripts/save_entities.py ``` **Output:** -``` -Creating new file: /path/to/project/.claude/guidelines.json -Added 1 new guideline(s). Total: 1 -Guidelines stored in: /path/to/project/.claude/guidelines.json +```text +Creating new file: /path/to/project/.claude/entities.json +Added 1 new entity(ies). Total: 1 +Entities stored in: /path/to/project/.claude/entities.json ``` -**Note:** Guidelines are also automatically saved when a conversation ends via the Stop hook. +**Note:** Entities are also automatically saved when a conversation ends via the Stop hook. -## Guideline Categories +## Entity Categories - **strategy**: High-level approach or methodology choices - **recovery**: Handling errors, edge cases, or unexpected situations @@ -110,7 +113,7 @@ Guidelines stored in: /path/to/project/.claude/guidelines.json ## Examples -### Good vs Bad Guidelines +### Good vs Bad Entities **BAD (reactive):** ```json @@ -132,8 +135,8 @@ Guidelines stored in: /path/to/project/.claude/guidelines.json ## Best Practices -1. **Be specific**: Generic guidelines are less useful than context-specific ones -2. **Be actionable**: Guidelines should clearly state what to do +1. **Be specific**: Generic entities are less useful than context-specific ones +2. **Be actionable**: Entities should clearly state what to do 3. **Include rationale**: Explain why the approach works 4. **Use situational triggers**: Context-based triggers are more useful than failure-based ones -5. **Limit to 3-5 guidelines**: Focus on the most impactful learnings +5. **Limit to 3-5 entities**: Focus on the most impactful learnings diff --git a/plugins/kaizen/skills/learn/scripts/save_entities.py b/plugins/kaizen/skills/learn/scripts/save_entities.py new file mode 100644 index 00000000..599bd554 --- /dev/null +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +""" +Save Entities Script +Reads entities from stdin and appends them to the entities file. +""" + +import getpass +import json +import os +import sys +from pathlib import Path +import datetime + +# Debug logging - use user-scoped directory for security +import tempfile + +def _get_log_dir(): + """Get user-scoped log directory with restrictive permissions.""" + try: + uid = os.getuid() + except AttributeError: + # Windows doesn't have os.getuid(); fall back to username + uid = getpass.getuser() + log_dir = os.path.join(tempfile.gettempdir(), f"kaizen-{uid}") + os.makedirs(log_dir, mode=0o700, exist_ok=True) + return log_dir + +LOG_FILE = os.path.join(_get_log_dir(), "kaizen-plugin.log") + +def log(message): + """Append a timestamped message to the log file.""" + if not os.environ.get("KAIZEN_DEBUG"): + return + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] [save] {message}\n") + +log("Script started") + + +def find_entities_file(): + """Find existing entities file, checking multiple locations.""" + # If KAIZEN_ENTITIES_FILE is explicitly set, honor it even if the file doesn't exist yet + env_val = os.environ.get("KAIZEN_ENTITIES_FILE") + if env_val: + return Path(env_val).resolve() + + # Fall back to checking other candidate locations + locations = [ + # Project root from Claude Code + os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/entities.json"), + # Current working directory + ".claude/entities.json", + # Plugin-relative path (fallback) + str(Path(__file__).parent.parent / "entities.json"), + ] + + for loc in locations: + if loc and Path(loc).exists(): + return Path(loc).resolve() + + return None + + +def get_default_entities_path(): + """Get default path for new entities file.""" + # Prefer project root if available + project_root = os.environ.get("CLAUDE_PROJECT_ROOT", "") + if project_root: + claude_dir = Path(project_root) / ".claude" + else: + # Fall back to current directory's .claude/ + claude_dir = Path(".claude") + + claude_dir.mkdir(parents=True, exist_ok=True) + return (claude_dir / "entities.json").resolve() + + +def load_existing_entities(path): + """Load existing entities from file. + + Returns: + list: The entities list on success or if file not found. + None: If the file exists but contains invalid JSON (to prevent data loss). + """ + try: + with open(path, encoding="utf-8") as f: + data = json.load(f) + return data.get("entities", []) + except FileNotFoundError: + return [] + except json.JSONDecodeError as e: + log(f"load_existing_entities: JSON decode error in {path}: {e}") + return None + + +def save_entities(path, entities): + """Save entities to file.""" + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump({"entities": entities}, f, indent=2) + f.write("\n") + + +def main(): + # Read entities from stdin + try: + input_data = json.load(sys.stdin) + log(f"Received input with keys: {list(input_data.keys())}") + except json.JSONDecodeError as e: + log(f"Failed to parse JSON input: {e}") + print(f"Error: Invalid JSON input - {e}", file=sys.stderr) + sys.exit(1) + + new_entities = input_data.get("entities", []) + if not new_entities: + log("No entities in input") + print("No entities provided in input.", file=sys.stderr) + sys.exit(0) + + log(f"Received {len(new_entities)} new entities") + + # Find or create entities file + existing_path = find_entities_file() + + if existing_path: + entities_path = existing_path + existing_entities = load_existing_entities(entities_path) + if existing_entities is None: + log(f"Refusing to overwrite corrupted file: {entities_path}") + print(f"Error: {entities_path} contains invalid JSON. " + "Fix or remove the file before adding entities.", file=sys.stderr) + sys.exit(1) + log(f"Found existing file: {entities_path} with {len(existing_entities)} entities") + print(f"Appending to existing file: {entities_path}") + else: + entities_path = get_default_entities_path() + existing_entities = [] + log(f"Creating new file: {entities_path}") + print(f"Creating new file: {entities_path}") + + # Merge entities (avoid duplicates by content) + existing_contents = {e.get("content") for e in existing_entities if e.get("content")} + added_count = 0 + + for entity in new_entities: + content = entity.get("content") + if not content: + log(f"Skipping entity without content: {entity}") + continue + if content not in existing_contents: + existing_entities.append(entity) + existing_contents.add(content) + added_count += 1 + + # Save merged entities + save_entities(entities_path, existing_entities) + + log(f"Added {added_count} new entities. Total: {len(existing_entities)}") + print(f"Added {added_count} new entity(ies). Total: {len(existing_entities)}") + print(f"Entities stored in: {entities_path}") + + +if __name__ == "__main__": + main() diff --git a/plugins/kaizen/skills/recall/SKILL.md b/plugins/kaizen/skills/recall/SKILL.md index c0bc0999..08a7a992 100644 --- a/plugins/kaizen/skills/recall/SKILL.md +++ b/plugins/kaizen/skills/recall/SKILL.md @@ -1,29 +1,29 @@ --- name: recall -description: Retrieves relevant guidelines from a knowledge base. Designed to be invoked automatically via hooks to inject context-appropriate guidelines before task execution. +description: Retrieves relevant entities from a knowledge base. Designed to be invoked automatically via hooks to inject context-appropriate entities before task execution. --- -# Guideline Retrieval +# Entity Retrieval ## Overview -This skill retrieves relevant guidelines from a stored knowledge base based on the current task context. It loads all stored guidelines and presents them to Claude for relevance filtering. +This skill retrieves relevant entities from a stored knowledge base based on the current task context. It loads all stored entities and presents them to Claude for relevance filtering. ## How It Works 1. Hook fires on user prompt submission 2. Script reads prompt from stdin (JSON with `prompt` field) -3. Loads all guidelines from the guidelines JSON file -4. Outputs formatted guidelines to stdout -5. Claude receives guidelines as additional context and applies relevant ones +3. Loads all entities from the entities JSON file +4. Outputs formatted entities to stdout +5. Claude receives entities as additional context and applies relevant ones -## Guidelines Storage +## Entities Storage -Guidelines are stored in `.claude/guidelines.json` in the project root: +Entities are stored in `.claude/entities.json` in the project root: ```json { - "guidelines": [ + "entities": [ { "content": "Use context managers for file operations", "rationale": "Ensures proper resource cleanup", diff --git a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py new file mode 100644 index 00000000..eddf7e80 --- /dev/null +++ b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Retrieve and output entities for Claude to filter.""" + +import getpass +import json +import os +import sys +from pathlib import Path +import datetime +import tempfile + + +def _get_log_dir(): + """Get user-scoped log directory with restrictive permissions.""" + try: + uid = os.getuid() + except AttributeError: + # Windows doesn't have os.getuid(); fall back to username + uid = getpass.getuser() + log_dir = os.path.join(tempfile.gettempdir(), f"kaizen-{uid}") + os.makedirs(log_dir, mode=0o700, exist_ok=True) + return log_dir + + +# Debug logging - use user-scoped directory for security +LOG_FILE = os.path.join(_get_log_dir(), "kaizen-plugin.log") + +def log(message): + """Append a timestamped message to the log file.""" + if not os.environ.get("KAIZEN_DEBUG"): + return + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] [retrieve] {message}\n") + +log("Script started") + +# Log all environment variables +log("=== Environment Variables ===") +for key, value in sorted(os.environ.items()): + # Mask sensitive values + if any(sensitive in key.upper() for sensitive in ['PASSWORD', 'SECRET', 'TOKEN', 'KEY', 'API']): + log(f" {key}=***MASKED***") + else: + log(f" {key}={value}") +log("=== End Environment Variables ===") + +# Log command-line arguments +log("=== Command-Line Arguments ===") +log(f" sys.argv: {sys.argv}") +log(f" Script path: {sys.argv[0] if sys.argv else 'N/A'}") +log(f" Arguments: {sys.argv[1:] if len(sys.argv) > 1 else 'None'}") +log("=== End Command-Line Arguments ===") + + +def find_entities_file(): + """Find the entities file in common locations.""" + # If KAIZEN_ENTITIES_FILE env var is set, it is authoritative - no fallbacks + entities_file_env = os.environ.get("KAIZEN_ENTITIES_FILE") + if entities_file_env: + path = Path(entities_file_env) + return path if path.exists() else None + + # Fallback locations when KAIZEN_ENTITIES_FILE is not set + locations = [ + # Project root from Claude Code + os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/entities.json"), + # Current working directory + ".claude/entities.json", + # Plugin-relative path (fallback) + str(Path(__file__).parent.parent / "entities.json"), + ] + for loc in locations: + if loc and Path(loc).exists(): + return Path(loc) + return None + + +def load_entities(): + """Load entities from the entities file. + + Returns: + list: The entities list on success or if file not found. + None: If the file exists but contains invalid JSON. + """ + entities_file = find_entities_file() + if not entities_file: + return [] + try: + with open(entities_file, encoding="utf-8") as f: + data = json.load(f) + return data.get("entities", []) + except IOError: + return [] + except json.JSONDecodeError as e: + log(f"load_entities: JSON decode error in {entities_file}: {e}") + return None + + +def format_entities(entities): + """Format all entities for Claude to review.""" + header = """## Entities for this task + +Review these entities and apply any relevant ones: + +""" + items = [] + for e in entities: + content = e.get('content') + if not content: + continue + item = f"- **[{e.get('category', 'general')}]** {content}" + if e.get('rationale'): + item += f"\n - _Rationale: {e['rationale']}_" + if e.get('trigger'): + item += f"\n - _When: {e['trigger']}_" + items.append(item) + + return header + "\n".join(items) + + +def main(): + # Read input from stdin (hook provides JSON with prompt) + try: + input_data = json.load(sys.stdin) + log("=== Input Data ===") + log(f" Keys: {list(input_data.keys())}") + log(f" Full content: {json.dumps(input_data, indent=2)}") + log("=== End Input Data ===") + except json.JSONDecodeError as e: + log(f"Failed to parse JSON input: {e}") + return + + # Load all entities + entities_file = find_entities_file() + log(f"Entities file: {entities_file}") + + entities = load_entities() + if entities is None: + log(f"Failed to load entities due to invalid JSON in {entities_file}") + print(f"Error: {entities_file} contains invalid JSON.", file=sys.stderr) + return + if not entities: + log("No entities found") + return + + log(f"Loaded {len(entities)} entities") + + # Output all entities - Claude will filter for relevance + output = format_entities(entities) + print(output) + log(f"Output {len(output)} chars to stdout") + + +if __name__ == "__main__": + main()