From 714feafea5304c25660482297a198728217d0157 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 00:02:15 -0600 Subject: [PATCH 01/11] feat: kaizen plugin improvements - Add skills package command for packaging .skill files - Move plugin scripts into their respective skill directories - Rename guidelines terminology to entities throughout - Make plugin logging conditional on KAIZEN_DEBUG env var - Use TMPDIR env var for plugin log file location - Rename plugin title from "Entities Plugin" to "Kaizen Plugin" in README - Remove standalone SAVE_SKILL.md documentation --- .gitignore | 1 + CLI.md | 36 +++ kaizen/cli/cli.py | 92 ++++++ plugins/kaizen/.claude-plugin/plugin.json | 4 +- plugins/kaizen/README.md | 53 ++-- plugins/kaizen/SAVE_SKILL.md | 291 ------------------ plugins/kaizen/hooks/hooks.json | 4 +- plugins/kaizen/scripts/save_guidelines.py | 131 -------- plugins/kaizen/skills/learn/SKILL.md | 54 ++-- .../skills/learn/scripts/save_entities.py | 133 ++++++++ plugins/kaizen/skills/recall/SKILL.md | 18 +- .../recall/scripts/retrieve_entities.py} | 70 +++-- 12 files changed, 364 insertions(+), 523 deletions(-) delete mode 100644 plugins/kaizen/SAVE_SKILL.md delete mode 100755 plugins/kaizen/scripts/save_guidelines.py create mode 100644 plugins/kaizen/skills/learn/scripts/save_entities.py rename plugins/kaizen/{scripts/retrieve_guidelines.py => skills/recall/scripts/retrieve_entities.py} (59%) mode change 100755 => 100644 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 bfa93266..a0271219 100644 --- a/kaizen/cli/cli.py +++ b/kaizen/cli/cli.py @@ -1,6 +1,8 @@ """Kaizen CLI for managing entities and namespaces.""" import json +import zipfile +from pathlib import Path from typing import Annotated, Optional import typer @@ -19,10 +21,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() @@ -372,5 +376,93 @@ 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 + 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 Exception as e: + console.print(f"[red]Failed to package {skill_name}: {e}[/red]") + + console.print(f"\n[bold green]Successfully packaged {packaged}/{len(skill_dirs)} skill(s)[/bold green]") + + 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..182911cb 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 +- `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/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..8db0711c 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,35 @@ 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)** ```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** ```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** ```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 +90,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 +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 +110,7 @@ Guidelines stored in: /path/to/project/.claude/guidelines.json ## Examples -### Good vs Bad Guidelines +### Good vs Bad Entities **BAD (reactive):** ```json @@ -132,8 +132,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..864f29a1 --- /dev/null +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +Save Entities Script +Reads entities from stdin and appends them to the entities file. +""" + +import json +import os +import sys +from pathlib import Path +import datetime + +# Debug logging +LOG_FILE = os.path.join(os.environ.get("TMPDIR", "/tmp"), "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") as f: + f.write(f"[{timestamp}] [save] {message}\n") + +log("Script started") + + +def find_entities_file(): + """Find existing entities file, checking multiple locations.""" + locations = [ + os.environ.get("ENTITIES_FILE"), + # 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.""" + try: + with open(path) as f: + data = json.load(f) + return data.get("entities", []) + except (json.JSONDecodeError, FileNotFoundError): + return [] + + +def save_entities(path, entities): + """Save entities to file.""" + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w") 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) + 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/scripts/retrieve_guidelines.py b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py old mode 100755 new mode 100644 similarity index 59% rename from plugins/kaizen/scripts/retrieve_guidelines.py rename to plugins/kaizen/skills/recall/scripts/retrieve_entities.py index e8b4b412..8fbce808 --- a/plugins/kaizen/scripts/retrieve_guidelines.py +++ b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Retrieve and output guidelines for Claude to filter.""" +"""Retrieve and output entities for Claude to filter.""" import json import os @@ -8,10 +8,12 @@ import datetime # Debug logging -LOG_FILE = "/tmp/guidelines-plugin.log" +LOG_FILE = os.path.join(os.environ.get("TMPDIR", "/tmp"), "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") as f: f.write(f"[{timestamp}] [retrieve] {message}\n") @@ -36,16 +38,16 @@ def log(message): log("=== End Command-Line Arguments ===") -def find_guidelines_file(): - """Find the guidelines file in common locations.""" +def find_entities_file(): + """Find the entities file in common locations.""" locations = [ - os.environ.get("GUIDELINES_FILE"), + os.environ.get("ENTITIES_FILE"), # Project root from Claude Code - os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/guidelines.json"), + os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/entities.json"), # Current working directory - ".claude/guidelines.json", + ".claude/entities.json", # Plugin-relative path (fallback) - str(Path(__file__).parent.parent / "guidelines.json"), + str(Path(__file__).parent.parent / "entities.json"), ] for loc in locations: if loc and Path(loc).exists(): @@ -53,36 +55,36 @@ def find_guidelines_file(): return None -def load_guidelines(): - """Load guidelines from the guidelines file.""" - guidelines_file = find_guidelines_file() - if not guidelines_file: +def load_entities(): + """Load entities from the entities file.""" + entities_file = find_entities_file() + if not entities_file: return [] try: - with open(guidelines_file) as f: + with open(entities_file) as f: data = json.load(f) - return data.get("guidelines", []) + return data.get("entities", []) except (json.JSONDecodeError, IOError): return [] -def format_guidelines(guidelines): - """Format all guidelines for Claude to review.""" - header = """## Guidelines for this task +def format_entities(entities): + """Format all entities for Claude to review.""" + header = """## Entities for this task -Review these guidelines and apply any relevant ones: +Review these entities and apply any relevant ones: """ items = [] - for g in guidelines: - content = g.get('content') + for e in entities: + content = e.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']}_" + 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) @@ -100,19 +102,19 @@ def main(): log(f"Failed to parse JSON input: {e}") return - # Load all guidelines - guidelines_file = find_guidelines_file() - log(f"Guidelines file: {guidelines_file}") + # Load all entities + entities_file = find_entities_file() + log(f"Entities file: {entities_file}") - guidelines = load_guidelines() - if not guidelines: - log("No guidelines found") + entities = load_entities() + if not entities: + log("No entities found") return - log(f"Loaded {len(guidelines)} guidelines") + log(f"Loaded {len(entities)} entities") - # Output all guidelines - Claude will filter for relevance - output = format_guidelines(guidelines) + # Output all entities - Claude will filter for relevance + output = format_entities(entities) print(output) log(f"Output {len(output)} chars to stdout") From cd5844d9ff2a844d3a427e1673d5540fa4d4761c Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 00:27:07 -0600 Subject: [PATCH 02/11] fix: use user-scoped temp directory for log file Replace shared /tmp path with per-user directory under tempfile.gettempdir() using os.getuid() for isolation. Create directory with 0o700 permissions to prevent unauthorized access to log files. --- plugins/kaizen/skills/learn/scripts/save_entities.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/kaizen/skills/learn/scripts/save_entities.py b/plugins/kaizen/skills/learn/scripts/save_entities.py index 864f29a1..c5409c71 100644 --- a/plugins/kaizen/skills/learn/scripts/save_entities.py +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -10,8 +10,16 @@ from pathlib import Path import datetime -# Debug logging -LOG_FILE = os.path.join(os.environ.get("TMPDIR", "/tmp"), "kaizen-plugin.log") +# Debug logging - use user-scoped directory for security +import tempfile + +def _get_log_dir(): + """Get user-scoped log directory with restrictive permissions.""" + log_dir = os.path.join(tempfile.gettempdir(), f"kaizen-{os.getuid()}") + 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.""" From 55cccd0574ca8c9b736dfeaff1f3b96c4d8da7c4 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 00:28:29 -0600 Subject: [PATCH 03/11] fix: honor ENTITIES_FILE env var even when file doesn't exist Previously find_entities_file would ignore a user-specified ENTITIES_FILE if the file didn't yet exist. Now it returns the specified path immediately, only falling back to other candidate locations when ENTITIES_FILE is not set. --- plugins/kaizen/skills/learn/scripts/save_entities.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/kaizen/skills/learn/scripts/save_entities.py b/plugins/kaizen/skills/learn/scripts/save_entities.py index c5409c71..887050bf 100644 --- a/plugins/kaizen/skills/learn/scripts/save_entities.py +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -34,8 +34,13 @@ def log(message): def find_entities_file(): """Find existing entities file, checking multiple locations.""" + # If ENTITIES_FILE is explicitly set, honor it even if the file doesn't exist yet + env_val = os.environ.get("ENTITIES_FILE") + if env_val: + return Path(env_val).resolve() + + # Fall back to checking other candidate locations locations = [ - os.environ.get("ENTITIES_FILE"), # Project root from Claude Code os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/entities.json"), # Current working directory From 1aff085f44173addeba5261594731c5dd44dc366 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 00:29:22 -0600 Subject: [PATCH 04/11] docs: convert method labels to proper markdown headings Replace bold emphasis with level-4 headings for Method 1/2/3 sections to fix MD036 lint warning (no emphasis instead of heading). --- plugins/kaizen/skills/learn/SKILL.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/kaizen/skills/learn/SKILL.md b/plugins/kaizen/skills/learn/SKILL.md index 8db0711c..d67c2e76 100644 --- a/plugins/kaizen/skills/learn/SKILL.md +++ b/plugins/kaizen/skills/learn/SKILL.md @@ -58,17 +58,20 @@ Output entities in the following JSON format: 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}/skills/learn/scripts/save_entities.py ``` -**Method 2: From File** +#### Method 2: From File + ```bash cat entities.json | python3 ${CLAUDE_PLUGIN_ROOT}/skills/learn/scripts/save_entities.py ``` -**Method 3: Interactive** +#### Method 3: Interactive + ```bash python3 ${CLAUDE_PLUGIN_ROOT}/skills/learn/scripts/save_entities.py # Then paste your JSON and press Ctrl+D From cebe9a971fed3bd4fd4d518a05cefc9ebfbf552a Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 00:30:54 -0600 Subject: [PATCH 05/11] docs: add language identifier to code fence to fix MD040 --- plugins/kaizen/skills/learn/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kaizen/skills/learn/SKILL.md b/plugins/kaizen/skills/learn/SKILL.md index d67c2e76..5117b3bd 100644 --- a/plugins/kaizen/skills/learn/SKILL.md +++ b/plugins/kaizen/skills/learn/SKILL.md @@ -97,7 +97,7 @@ echo '{ ``` **Output:** -``` +```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 From 8f3165b1ddf13b29058c3773ed7db26287bfaf28 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 00:41:17 -0600 Subject: [PATCH 06/11] fix: add cross-platform support and explicit UTF-8 encoding Update save_entities.py and retrieve_entities.py to handle Windows compatibility by catching AttributeError on os.getuid() and falling back to getpass.getuser(). Add explicit encoding="utf-8" to all file I/O operations for deterministic behavior across platforms. --- .../skills/learn/scripts/save_entities.py | 14 +++++++---- .../recall/scripts/retrieve_entities.py | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/plugins/kaizen/skills/learn/scripts/save_entities.py b/plugins/kaizen/skills/learn/scripts/save_entities.py index 887050bf..b7879a3e 100644 --- a/plugins/kaizen/skills/learn/scripts/save_entities.py +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -4,6 +4,7 @@ Reads entities from stdin and appends them to the entities file. """ +import getpass import json import os import sys @@ -15,7 +16,12 @@ def _get_log_dir(): """Get user-scoped log directory with restrictive permissions.""" - log_dir = os.path.join(tempfile.gettempdir(), f"kaizen-{os.getuid()}") + 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 @@ -26,7 +32,7 @@ def log(message): if not os.environ.get("KAIZEN_DEBUG"): return timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - with open(LOG_FILE, "a") as f: + with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[{timestamp}] [save] {message}\n") log("Script started") @@ -73,7 +79,7 @@ def get_default_entities_path(): def load_existing_entities(path): """Load existing entities from file.""" try: - with open(path) as f: + with open(path, encoding="utf-8") as f: data = json.load(f) return data.get("entities", []) except (json.JSONDecodeError, FileNotFoundError): @@ -83,7 +89,7 @@ def load_existing_entities(path): def save_entities(path, entities): """Save entities to file.""" path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "w") as f: + with open(path, "w", encoding="utf-8") as f: json.dump({"entities": entities}, f, indent=2) f.write("\n") diff --git a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py index 8fbce808..4c50eba3 100644 --- a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py +++ b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py @@ -1,21 +1,36 @@ #!/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 -# Debug logging -LOG_FILE = os.path.join(os.environ.get("TMPDIR", "/tmp"), "kaizen-plugin.log") + +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") as f: + with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[{timestamp}] [retrieve] {message}\n") log("Script started") @@ -61,7 +76,7 @@ def load_entities(): if not entities_file: return [] try: - with open(entities_file) as f: + with open(entities_file, encoding="utf-8") as f: data = json.load(f) return data.get("entities", []) except (json.JSONDecodeError, IOError): From ddb3b47dfa47ecf330a49aedd12a506ccc0be403 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 15:32:21 -0600 Subject: [PATCH 07/11] fix: improve error handling in skill packaging loop Narrow exception handling to specific zip/file errors and track failures separately. Exit with non-zero status when any skills fail to package. --- kaizen/cli/cli.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kaizen/cli/cli.py b/kaizen/cli/cli.py index 206d94ac..46f2593d 100644 --- a/kaizen/cli/cli.py +++ b/kaizen/cli/cli.py @@ -1,6 +1,7 @@ """Kaizen CLI for managing entities and namespaces.""" import json +import sys import zipfile from pathlib import Path from typing import Annotated, Optional @@ -446,6 +447,7 @@ def package_skills( # Package each skill packaged = 0 + failed = 0 for skill_name, skill_path in skill_dirs: output_file = output / f"{skill_name}.skill" @@ -460,11 +462,15 @@ def package_skills( console.print(f"[green]Packaged:[/green] {skill_name} -> {output_file}") packaged += 1 - except Exception as e: + except (OSError, PermissionError, zipfile.LargeZipFile, zipfile.BadZipFile, ValueError) as e: console.print(f"[red]Failed to package {skill_name}: {e}[/red]") + failed += 1 console.print(f"\n[bold green]Successfully packaged {packaged}/{len(skill_dirs)} skill(s)[/bold green]") + if failed > 0: + sys.exit(1) + if __name__ == "__main__": app() From 1463c182631bcc9cb66e4939c208384bae367a45 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 15:36:47 -0600 Subject: [PATCH 08/11] refactor: rename ENTITIES_FILE to KAIZEN_ENTITIES_FILE and make it authoritative When KAIZEN_ENTITIES_FILE env var is set, use only that path without fallbacks. This provides explicit control over entity file location and follows project naming conventions. --- plugins/kaizen/skills/recall/scripts/retrieve_entities.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py index 4c50eba3..55f40b94 100644 --- a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py +++ b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py @@ -55,8 +55,14 @@ def log(message): 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 = [ - os.environ.get("ENTITIES_FILE"), # Project root from Claude Code os.path.join(os.environ.get("CLAUDE_PROJECT_ROOT", ""), ".claude/entities.json"), # Current working directory From c1445ca3e0414a763307b9605df36a1cb660f865 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 15:40:12 -0600 Subject: [PATCH 09/11] fix: prevent data loss by not swallowing JSONDecodeError in entity loading Return None instead of [] when entities file contains invalid JSON. Callers now detect this and refuse to overwrite corrupted files, displaying an error message instead of silently treating them as empty. --- .../skills/learn/scripts/save_entities.py | 17 +++++++++++++++-- .../skills/recall/scripts/retrieve_entities.py | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/plugins/kaizen/skills/learn/scripts/save_entities.py b/plugins/kaizen/skills/learn/scripts/save_entities.py index b7879a3e..7be85995 100644 --- a/plugins/kaizen/skills/learn/scripts/save_entities.py +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -77,13 +77,21 @@ def get_default_entities_path(): def load_existing_entities(path): - """Load existing entities from file.""" + """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 (json.JSONDecodeError, FileNotFoundError): + 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): @@ -118,6 +126,11 @@ def main(): 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: diff --git a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py index 55f40b94..eddf7e80 100644 --- a/plugins/kaizen/skills/recall/scripts/retrieve_entities.py +++ b/plugins/kaizen/skills/recall/scripts/retrieve_entities.py @@ -77,7 +77,12 @@ def find_entities_file(): def load_entities(): - """Load entities from the entities file.""" + """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 [] @@ -85,8 +90,11 @@ def load_entities(): with open(entities_file, encoding="utf-8") as f: data = json.load(f) return data.get("entities", []) - except (json.JSONDecodeError, IOError): + 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): @@ -128,6 +136,10 @@ def main(): 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 From f79abfa9ce4f6d0aadfed4b2f9193460124daf7a Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 15:42:52 -0600 Subject: [PATCH 10/11] fix: use KAIZEN_ENTITIES_FILE consistently in save_entities.py Update save_entities.py to use KAIZEN_ENTITIES_FILE instead of ENTITIES_FILE to match retrieve_entities.py. Also update README documentation to reflect the correct environment variable name. --- plugins/kaizen/README.md | 2 +- plugins/kaizen/skills/learn/scripts/save_entities.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/kaizen/README.md b/plugins/kaizen/README.md index 182911cb..8d849b32 100644 --- a/plugins/kaizen/README.md +++ b/plugins/kaizen/README.md @@ -91,7 +91,7 @@ Entities are stored in `.claude/entities.json`: ## Environment Variables -- `ENTITIES_FILE`: Override the default entities storage location +- `KAIZEN_ENTITIES_FILE`: Override the default entities storage location - `CLAUDE_PROJECT_ROOT`: Set by Claude Code, used to locate project-level entities ## Verification diff --git a/plugins/kaizen/skills/learn/scripts/save_entities.py b/plugins/kaizen/skills/learn/scripts/save_entities.py index 7be85995..599bd554 100644 --- a/plugins/kaizen/skills/learn/scripts/save_entities.py +++ b/plugins/kaizen/skills/learn/scripts/save_entities.py @@ -40,8 +40,8 @@ def log(message): def find_entities_file(): """Find existing entities file, checking multiple locations.""" - # If ENTITIES_FILE is explicitly set, honor it even if the file doesn't exist yet - env_val = os.environ.get("ENTITIES_FILE") + # 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() From bb6a53c1b2d09e72751c65bfd66e1c53ea4bcf73 Mon Sep 17 00:00:00 2001 From: Vinod Muthusamy Date: Mon, 2 Feb 2026 15:45:41 -0600 Subject: [PATCH 11/11] fix: show accurate summary message in skill packaging Only display success message when all skills packaged successfully. When failures occur, show partial success count with failure count before exiting with code 1. --- kaizen/cli/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kaizen/cli/cli.py b/kaizen/cli/cli.py index 46f2593d..91dad297 100644 --- a/kaizen/cli/cli.py +++ b/kaizen/cli/cli.py @@ -466,9 +466,10 @@ def package_skills( console.print(f"[red]Failed to package {skill_name}: {e}[/red]") failed += 1 - console.print(f"\n[bold green]Successfully packaged {packaged}/{len(skill_dirs)} skill(s)[/bold green]") - - if failed > 0: + 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)