diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c5f19f54..7af9eef0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,20 +8,27 @@ }, "plugins": [ { - "name": "suggest-compacting", - "description": "Automatically suggests when to manually compact context during long sessions", - "source": "./plugins/suggest-compacting", + "name": "everything-agent", + "description": "AI coding assistant toolkit - development automation, LSP, memory, project management, and workflow tools", + "source": "./", "category": "development", "tags": [ + "lsp", + "git", "workflow", - "context-management", - "hooks" + "automation", + "tdd", + "debugging", + "memory", + "jira", + "databricks", + "mcp" ], - "version": "5.23.3" + "version": "5.23.1" }, { "name": "git-guard", - "description": "Git workflow protection hooks - prevents commit and PR bypasses, enforces pre-commit checks", + "description": "Git workflow protection hooks - prevents commit and PR bypasses", "source": "./plugins/git-guard", "category": "development", "tags": [ @@ -35,7 +42,7 @@ }, { "name": "lsp-bash", - "description": "Language Server Protocol support for Bash - bash-language-server", + "description": "Bash Language Server integration for Claude Code", "source": "./plugins/lsp-bash", "category": "development", "tags": [ @@ -48,7 +55,7 @@ }, { "name": "lsp-typescript", - "description": "Language Server Protocol support for TypeScript and JavaScript - typescript-language-server", + "description": "TypeScript/JavaScript Language Server integration for Claude Code", "source": "./plugins/lsp-typescript", "category": "development", "tags": [ @@ -62,7 +69,7 @@ }, { "name": "lsp-python", - "description": "Language Server Protocol support for Python - pyright", + "description": "Python Language Server integration using Pyright for Claude Code", "source": "./plugins/lsp-python", "category": "development", "tags": [ @@ -75,7 +82,7 @@ }, { "name": "lsp-go", - "description": "Language Server Protocol support for Go - gopls", + "description": "Language Server Protocol support for Go - provides gopls integration for Go code completion, diagnostics, and navigation", "source": "./plugins/lsp-go", "category": "development", "tags": [ @@ -88,7 +95,7 @@ }, { "name": "lsp-kotlin", - "description": "Language Server Protocol support for Kotlin - kotlin-language-server", + "description": "Language Server Protocol support for Kotlin - code completion, diagnostics, go-to-definition, and other IDE features", "source": "./plugins/lsp-kotlin", "category": "development", "tags": [ @@ -101,7 +108,7 @@ }, { "name": "lsp-lua", - "description": "Language Server Protocol support for Lua - lua-language-server", + "description": "Lua Language Server Protocol support - lua-language-server for .lua files", "source": "./plugins/lsp-lua", "category": "development", "tags": [ @@ -114,7 +121,7 @@ }, { "name": "lsp-nix", - "description": "Language Server Protocol support for Nix - nil", + "description": "Language Server Protocol support for Nix - provides autocomplete, diagnostics, and code navigation for Nix files", "source": "./plugins/lsp-nix", "category": "development", "tags": [ @@ -125,34 +132,6 @@ ], "version": "5.23.3" }, - { - "name": "me", - "description": "Personal Claude Code configuration - TDD, systematic debugging, git workflow, code review, development automation", - "source": "./plugins/me", - "category": "development", - "tags": [ - "workflow", - "automation", - "tdd", - "git", - "code-review", - "debugging" - ], - "version": "5.23.3" - }, - { - "name": "ralph-loop", - "description": "Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique", - "source": "./plugins/ralph-loop", - "category": "development", - "tags": [ - "automation", - "iteration", - "loop", - "ai" - ], - "version": "5.23.3" - }, { "name": "handoff", "description": "Session handoff plugin for Claude Code - save and restore session context between sessions", @@ -168,7 +147,7 @@ }, { "name": "databricks-devtools", - "description": "Databricks CLI wrapper for workspace management and SQL execution", + "description": "Databricks SQL schema explorer for Unity Catalog discovery", "source": "./plugins/databricks-devtools", "category": "development", "tags": [ @@ -182,7 +161,7 @@ }, { "name": "jira", - "description": "Jira and Confluence integration via Atlassian MCP server - issue management, status reports, meeting notes, and backlog creation", + "description": "Jira integration for issue tracking and project management. Triage bugs, capture tasks from notes, generate status reports, and automate Jira workflows using Atlassian's MCP server.", "source": "./plugins/jira", "category": "productivity", "tags": [ diff --git a/.gitignore b/.gitignore index 1779883f..a547e11c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ lib64/ # Exception: Include compiled/built output for specific plugins !plugins/conversation-memory/dist/ -!plugins/suggest-compacting/dist/ parts/ sdist/ diff --git a/.releaserc.js b/.releaserc.js index 4cb41f8c..aa803834 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -2,18 +2,33 @@ import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs'; import { resolve } from 'path'; /** - * Dynamically discover all plugins from the plugins directory. + * Dynamically discover all plugins from both root and plugins directory. * A plugin is identified by the presence of .claude-plugin/plugin.json. + * + * Supports two types of plugins: + * - Root canonical plugin: ./.claude-plugin/plugin.json (source: "./") + * - Plugins directory: ./plugins//.claude-plugin/plugin.json (source: "./plugins/") + * + * @returns {string[]} Array of plugin identifiers ('.' for root, name for plugins/*) */ function discoverPlugins() { - const pluginsDir = resolve(process.cwd(), 'plugins'); const plugins = []; - for (const entry of readdirSync(pluginsDir, { withFileTypes: true })) { - if (entry.isDirectory()) { - const pluginJsonPath = resolve(pluginsDir, entry.name, '.claude-plugin/plugin.json'); - if (existsSync(pluginJsonPath)) { - plugins.push(entry.name); + // Check for root canonical plugin + const rootPluginJsonPath = resolve(process.cwd(), '.claude-plugin/plugin.json'); + if (existsSync(rootPluginJsonPath)) { + plugins.push('.'); // Use '.' to indicate root plugin + } + + // Check plugins directory + const pluginsDir = resolve(process.cwd(), 'plugins'); + if (existsSync(pluginsDir)) { + for (const entry of readdirSync(pluginsDir, { withFileTypes: true })) { + if (entry.isDirectory()) { + const pluginJsonPath = resolve(pluginsDir, entry.name, '.claude-plugin/plugin.json'); + if (existsSync(pluginJsonPath)) { + plugins.push(entry.name); + } } } } @@ -21,12 +36,26 @@ function discoverPlugins() { return plugins; } +/** + * Get the plugin.json path for a given plugin identifier. + * @param {string} plugin - Plugin identifier ('.' for root, name for plugins/*) + * @returns {string} Absolute path to plugin.json + */ +function getPluginJsonPath(plugin) { + if (plugin === '.') { + return resolve(process.cwd(), '.claude-plugin/plugin.json'); + } + return resolve(process.cwd(), `plugins/${plugin}/.claude-plugin/plugin.json`); +} + /** * Custom plugin to update version in plugin.json files. * * Ensures all plugin.json versions are synchronized with marketplace.json * during the release process. Each plugin has its own plugin.json that needs * to be updated to the same version. + * + * Supports both root canonical plugin ('.') and plugins directory plugins. */ function updatePluginJsons() { return { @@ -43,15 +72,12 @@ function updatePluginJsons() { // Check each plugin.json for (const plugin of plugins) { - const pluginJsonPath = resolve( - process.cwd(), - `plugins/${plugin}/.claude-plugin/plugin.json` - ); + const pluginJsonPath = getPluginJsonPath(plugin); const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf8')); if (pluginJson.version !== lastVersion) { mismatches.push({ - plugin, + plugin: plugin === '.' ? '(root)' : plugin, current: pluginJson.version, expected: lastVersion, }); @@ -88,10 +114,7 @@ function updatePluginJsons() { const plugins = discoverPlugins(); for (const plugin of plugins) { - const pluginJsonPath = resolve( - process.cwd(), - `plugins/${plugin}/.claude-plugin/plugin.json` - ); + const pluginJsonPath = getPluginJsonPath(plugin); const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf8')); pluginJson.version = version; writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n'); @@ -135,6 +158,7 @@ const plugins = [ '@semantic-release/git', { assets: [ + '.claude-plugin/plugin.json', 'plugins/*/.claude-plugin/plugin.json', '.claude-plugin/marketplace.json', ], diff --git a/README.md b/README.md index a5369b52..6d977560 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,28 @@ AI coding assistant toolkit - Claude Code, OpenCode, and more. +This is the **canonical plugin** that provides a unified toolkit for AI-assisted development. + ## Available Plugins Plugins are automatically discovered from the `plugins/` directory. For detailed information about each plugin, see the respective plugin's README.md file. -- **ralph-loop**: Implementation of the Ralph Wiggum technique for iterative, - self-referential AI development loops - **git-guard**: Git workflow protection hooks that prevent commit and PR bypasses (automatic, no commands needed) -- **me**: Personal development workflow automation with 8 commands, 1 agent, - and 7 skills (TDD, debugging, git, code review, research, orchestration) - **jira**: Jira integration with 5 powerful skills - triage bugs, capture tasks from meeting notes, generate status reports, search company knowledge, and convert specs to backlogs. Uses Atlassian's MCP server with OAuth 2.1. -- **strategic-compact**: Strategic content compaction and organization tools - (automatic PreToolUse hook) +- **databricks-devtools**: Databricks development tools with CLI commands, + workspace sync, and run management +- **handoff**: Handoff workflow for seamless task transitions between agents +- **lsp-bash**: Language Server Protocol integration for Bash +- **lsp-go**: Language Server Protocol integration for Go +- **lsp-kotlin**: Language Server Protocol integration for Kotlin +- **lsp-lua**: Language Server Protocol integration for Lua +- **lsp-nix**: Language Server Protocol integration for Nix +- **lsp-python**: Language Server Protocol integration for Python +- **lsp-typescript**: Language Server Protocol integration for TypeScript ## Quick Start @@ -28,21 +34,8 @@ information about each plugin, see the respective plugin's README.md file. claude plugin marketplace add https://github.com/baleen37/everything-agent # Install a plugin -claude plugin install ralph-loop@everything-agent claude plugin install git-guard@everything-agent -``` - -### Using Ralph Loop - -```bash -# Start Claude Code with ralph-loop -claude - -# In Claude Code, start a loop -/ralph-loop "Build a REST API for todos with tests" --max-iterations 20 --completion-promise "COMPLETE" - -# Cancel if needed -/cancel-ralph +claude plugin install jira@everything-agent ``` ### Using Git Guard @@ -53,61 +46,27 @@ Git Guard operates automatically via PreToolUse hooks - no commands needed: - Pre-commit validation is enforced - Works transparently in the background -### Using "me" Plugin (Personal Workflow) - -The "me" plugin provides comprehensive development workflow automation: - -**Commands (8):** - -- `/brainstorm` - Brainstorming and feature planning -- `/create-pr` - Full git workflow (commit → push → PR) -- `/debug` - Systematic debugging process -- `/orchestrate` - Sequential agent workflow execution -- `/refactor-clean` - Code refactoring and cleanup -- `/research` - Web research with citations -- `/sdd` - Subagent-driven development approach -- `/verify` - Comprehensive codebase verification - -**Agents (1):** - -- `code-reviewer` - Code review against plans and standards - -**Skills (7):** - -- `ci-troubleshooting` - Systematic CI debugging -- `test-driven-development` - TDD methodology -- `systematic-debugging` - Root cause analysis -- `using-git-worktrees` - Isolated feature work -- `setup-precommit-and-ci` - Pre-commit and CI setup -- `nix-direnv-setup` - Nix flake direnv integration -- `writing-claude-code` - Creating Claude Code components - ## Project Structure +This project uses a **hybrid structure** with both a root plugin (everything-agent) +and individual plugins in the `plugins/` directory. + ```text everything-agent/ ├── .claude-plugin/ │ └── marketplace.json # Marketplace configuration (everything-agent) -├── plugins/ # Plugins (auto-discovered) -│ ├── ralph-loop/ # Ralph Wiggum technique implementation -│ │ ├── commands/ # Slash commands (/ralph-init, /ralph-loop, /cancel-ralph, /help) -│ │ ├── scripts/ # Core loop script (ralph.sh) and prompt template -│ │ └── tests/ # BATS tests +├── plugins/ # Individual plugins (auto-discovered) │ ├── git-guard/ # Git workflow protection │ │ ├── hooks/ # PreToolUse hook │ │ └── tests/ # BATS tests -│ ├── me/ # Personal workflow automation -│ │ ├── commands/ # 8 commands (brainstorm, create-pr, debug, etc.) -│ │ ├── agents/ # Code reviewer agent -│ │ ├── skills/ # 7 skills (ci-troubleshooting, tdd, etc.) -│ │ └── tests/ # BATS tests │ ├── jira/ # Jira integration │ │ └── agents/ # Jira MCP agents -│ └── strategic-compact/ # Content compaction -│ └── hooks/ # PreToolUse hook (suggests compaction) +│ ├── databricks-devtools/ # Databricks development tools +│ ├── handoff/ # Task handoff workflow +│ └── lsp-*/ # Language Server Protocol integrations ├── .github/workflows/ # CI/CD workflows ├── docs/ # Development and testing documentation -├── tests/ # BATS tests +├── tests/ # BATS tests (root + plugin tests) ├── schemas/ # JSON schemas └── CLAUDE.md # Project instructions for Claude Code ``` @@ -164,14 +123,14 @@ git commit -m "type(scope): description" - `fix`: Bug fix (patch version bump) - `docs`, `style`, `refactor`, `test`, `build`, `ci`, `chore`: No version bump -**Scope:** Plugin name (`ralph-loop`, `git-guard`, etc.) +**Scope:** Plugin name (`git-guard`, `jira`, `databricks-devtools`, etc.) **Examples:** ```text -feat(ralph-loop): add iteration progress tracking -fix(git-guard): prevent --no-verify bypass -docs(me): update skill documentation +feat(git-guard): add new pre-commit validation +fix(jira): fix OAuth authentication flow +docs(databricks-devtools): update CLI documentation ``` #### Release Process @@ -199,15 +158,6 @@ See `plugins/jira/README.md` for detailed setup instructions. - **Skills** (`skills/*/SKILL.md`): Context-aware guides that activate automatically - **Hooks** (`hooks/hooks.json` + `hooks/*.sh`): Event-driven automation (SessionStart, PreToolUse, etc.) -## Ralph Loop Philosophy - -Ralph embodies several key principles: - -1. **Iteration > Perfection**: Don't aim for perfect on first try. Let the loop refine the work. -2. **Failures Are Data**: "Deterministically bad" means failures are predictable and informative. -3. **Operator Skill Matters**: Success depends on writing good prompts, not just having a good model. -4. **Persistence Wins**: Keep trying until success. - ## Pre-commit Hooks This project uses pre-commit hooks for code quality: diff --git a/agents/ralph/config.sh.example b/agents/ralph/config.sh.example deleted file mode 100644 index aabdb21a..00000000 --- a/agents/ralph/config.sh.example +++ /dev/null @@ -1,7 +0,0 @@ -# Ralph Configuration File -# Copy this file to .agents/ralph/config.sh to customize Ralph's behavior - -# STALE_SECONDS: Number of seconds after which a PRD is considered "stale" -# Default: 86400 (24 hours) -# Example: Set to 43200 for 12 hours, or 172800 for 48 hours -# STALE_SECONDS=86400 diff --git a/commands/cancel-ralph.md b/commands/cancel-ralph.md deleted file mode 100644 index 30330de8..00000000 --- a/commands/cancel-ralph.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -description: "Cancel active Ralph loop" -allowed-tools: ["Bash(kill*)", "Bash(cat .ralph/ralph.pid)", "Bash(rm .ralph/ralph.pid)"] ---- - -# Cancel Ralph - -Cancel the active Ralph loop by killing the ralph.sh process. - -1. Check if `.ralph/ralph.pid` exists -2. If not found: report "No active Ralph loop" -3. If found: - - Read the PID from the file - - Kill the process - - Remove the PID file - - Report cancellation diff --git a/commands/ralph-help.md b/commands/ralph-help.md deleted file mode 100644 index 3536f489..00000000 --- a/commands/ralph-help.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -description: "Explain Ralph Wiggum technique and available commands" ---- - -# Ralph Wiggum Plugin Help - -Please explain the following to the user: - -## What is the Ralph Wiggum Technique? - -The Ralph Wiggum technique is an iterative development methodology based on continuous -AI loops, pioneered by Geoffrey Huntley and the snarktank community (inspired by the -original snarktank/ralph project). - -**Core concept:** - -```bash -while :; do - claude --print PROMPT.md -done -``` - -The same prompt is fed to Claude repeatedly. The "self-referential" aspect comes -from Claude seeing its own previous work in the files and git history, not from -feeding output back as input. - -**Each iteration:** - -1. A fresh Claude instance receives the SAME prompt -2. Works on the task, modifying files -3. Exits (process completes) -4. Bash loop spawns another fresh instance -5. Claude sees its previous work in the files -6. Iteratively improves until completion - -The technique is described as "deterministically bad in an undeterministic world" - -failures are predictable, enabling systematic improvement through prompt tuning. - -## Available Commands - -### /ralph-init "description" - -Create a PRD (Product Requirements Document) from a task description. - -**Usage:** - -```text -/ralph-init "Build a REST API for todos" -``` - -**How it works:** - -1. Analyzes the task description -2. Breaks it into user stories (completable in one iteration) -3. Creates `.ralph/prd.json` with stories and acceptance criteria -4. Creates `.ralph/progress.txt` for tracking -5. Reports the story breakdown - ---- - -### /ralph-loop [max-iterations] - -Start the Ralph loop with a bash script. - -**Usage:** - -```text -/ralph-loop -/ralph-loop 20 -``` - -**How it works:** - -1. Reads `.ralph/prd.json` for user stories -2. Spawns fresh `claude --print` instances in a bash loop -3. Each instance implements one story, runs tests, commits if passing -4. Loop exits when all stories pass or max iterations reached -5. Progress tracked in `.ralph/progress.txt` - -**Default:** 10 iterations - -**Monitoring progress:** - -```bash -tail -f .ralph/progress.txt -``` - ---- - -### /cancel-ralph - -Cancel an active Ralph loop. - -**Usage:** - -```text -/cancel-ralph -``` - -**How it works:** - -- Checks for `.ralph/ralph.pid` (loop process ID) -- Kills the bash loop process -- Removes PID file -- Reports cancellation - ---- - -## How It Works - -### The New Architecture - -This plugin implements Ralph using a **bash loop + fresh instances** approach: - -1. **Initialize**: Run `/ralph-init` to create PRD with user stories -2. **Start loop**: Run `/ralph-loop` to begin bash loop -3. **Iterate**: Each iteration spawns a fresh Claude instance -4. **Track**: Progress logged to `.ralph/progress.txt` -5. **Complete**: Loop exits when all stories pass or max iterations reached - -### Self-Reference Mechanism - -The "loop" doesn't mean Claude talks to itself. It means: - -- Same prompt repeated across fresh instances -- Claude's work persists in files and git history -- Each iteration sees previous attempts -- Builds incrementally toward goal - -### Fresh Instance Benefits - -- Clean state each iteration (no session pollution) -- Natural exit after each story -- Better error isolation -- Easier debugging - -## Example - -### Building a Feature - -```text -/ralph-init "Add user authentication with JWT" -``` - -Claude breaks this into stories: - -- US-001: Create JWT utilities -- US-002: Add login endpoint -- US-003: Add auth middleware -- US-004: Write tests - -```text -/ralph-loop 20 -``` - -The loop: - -- Implements US-001, runs tests, commits -- Implements US-002, runs tests, commits -- Continues until all stories pass or 20 iterations reached - -## When to Use Ralph - -**Good for:** - -- Well-defined tasks with clear success criteria -- Tasks requiring iteration and refinement -- Test-driven development workflows -- Greenfield projects -- Features that can be broken into small stories - -**Not good for:** - -- Tasks requiring human judgment or design decisions -- One-shot operations -- Tasks with unclear success criteria -- Debugging production issues (use targeted debugging instead) - -## Learn More - -- Original technique: -- Inspiration: diff --git a/commands/ralph-init.md b/commands/ralph-init.md deleted file mode 100644 index d07a36fe..00000000 --- a/commands/ralph-init.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -description: "Initialize a PRD for Ralph loop execution" ---- - -# Ralph Init - -You are initializing a Ralph loop PRD (Product Requirements Document). - -The user's task description: $ARGUMENTS - -## Your Job - -1. Analyze the task description -2. Break it into well-sized user stories (each completable in one focused iteration) -3. Create `.ralph/prd.json` with the structure below -4. Create `.ralph/progress.txt` with initial content -5. Report the summary - -## `.ralph/prd.json` Format - -```json -{ - "project": "[Project name from context]", - "branchName": "ralph/[feature-name-kebab-case]", - "description": "[Task description]", - "userStories": [ - { - "id": "US-001", - "title": "[Short title]", - "description": "As a [user], I want to [action] so that [benefit].", - "acceptanceCriteria": [ - "Specific criterion 1", - "Tests pass", - "Typecheck passes" - ], - "priority": 1, - "status": "open", - "startedAt": null, - "completedAt": null, - "passes": false - } - ] -} -``` - -**Status field values:** - -- `"open"` - Story not yet started (default) -- `"in_progress"` - AI is actively working on this story -- `"done"` - Story completed successfully - -**Timestamp fields:** - -- `startedAt` - ISO timestamp when status changes to "in_progress", null otherwise -- `completedAt` - ISO timestamp when status changes to "done", null otherwise -- `passes` - Boolean, true when story passes all acceptance criteria (kept for backward compatibility) - -## `.ralph/progress.txt` Initial Content - -```text -# Ralph Progress Log -Started: [ISO timestamp] - -## Codebase Patterns -(No patterns discovered yet) - ---- -``` - -## Story Guidelines - -- **Right-sized**: Each story should be completable in one focused session -- **Verifiable**: Include "Tests pass" and "Typecheck passes" in acceptance criteria -- **Independent**: Minimize dependencies between stories -- **Priority order**: Foundational work (DB, types, models) before features, features before UI -- **Include testing stories**: If the task needs tests, make them explicit stories - -## After Creating Files - -Report: - -- Total number of stories -- Story list with IDs and titles -- Suggested command: `/ralph-loop` or `/ralph-loop --max-iterations N` diff --git a/commands/ralph-loop.md b/commands/ralph-loop.md deleted file mode 100644 index c7491013..00000000 --- a/commands/ralph-loop.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -description: "Start Ralph loop in current session" -allowed-tools: ["Bash(${CLAUDE_PLUGIN_ROOT}/scripts/ralph.sh*)"] ---- - -# Ralph Loop - -Execute the Ralph loop script. This will spawn fresh Claude instances in a bash loop. - -## Prerequisites - -- `.ralph/prd.json` must exist (run `/ralph-init` first) - -## Usage - -Default (10 iterations): - -```! -"${CLAUDE_PLUGIN_ROOT}/scripts/ralph.sh" -``` - -With custom max iterations: - -```! -"${CLAUDE_PLUGIN_ROOT}/scripts/ralph.sh" $ARGUMENTS -``` - -## What Happens - -1. Script reads `.ralph/prd.json` for user stories -2. For each iteration, spawns a fresh `claude --print` instance -3. Each instance implements one user story, runs tests, commits if passing -4. Loop exits when all stories pass or max iterations reached -5. Progress is tracked in `.ralph/progress.txt` - -## Monitoring - -Watch progress in another terminal: - -```bash -tail -f .ralph/progress.txt -``` - -To cancel: `/cancel-ralph` diff --git a/docs/TESTING.md b/docs/TESTING.md index 88a32818..9d0c1ca0 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -13,7 +13,7 @@ bash tests/run-all-tests.sh This script runs: 1. Root tests in `tests/` -2. All plugin tests in `plugins/*/tests/` +2. Plugin tests in `tests/{plugin-name}/` ### Root Tests Only @@ -45,21 +45,19 @@ bats tests/performance/benchmarks.bats ### Test Count -- Unit Tests: 148 tests -- Integration Tests: 20 tests (10 plugin loading + 10 cross-plugin interactions) -- Performance Tests: 10 tests -- Error Handling Tests: 23 tests -- Edge Case Tests: 17 tests -- Negative Tests: 17 tests -- **Total: 178 tests** +Tests run from both root `tests/` directory and individual plugin test directories: + +- Root Tests: Various unit, integration, and validation tests +- Plugin Tests: Tests for git-guard, jira, databricks-devtools, handoff, lsp-* +- **Total: 163 tests** ### Individual Plugin Tests ```bash -bats plugins/git-guard/tests/ -bats plugins/me/tests/ -bats plugins/ralph-loop/tests/ -bats plugins/strategic-compact/tests/ +bats tests/git-guard/ +bats tests/jira/ +bats tests/databricks-devtools/ +bats tests/handoff/ ``` ### Verbose Output @@ -212,13 +210,6 @@ Tests for boundary conditions: - Very long field values - Minimal/maximum valid configurations -Performance benchmarks for critical operations: - -- Plugin list caching effectiveness -- JSON parsing speed -- File operation efficiency -- Test execution timing - ## Test Helpers Tests use `tests/helpers/bats_helper.bash` which provides: diff --git a/plugins/me/.claude-plugin/plugin.json b/plugins/me/.claude-plugin/plugin.json deleted file mode 100644 index 9ea8e1b5..00000000 --- a/plugins/me/.claude-plugin/plugin.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "me", - "version": "5.23.3", - "description": "Personal Claude Code configuration - TDD, systematic debugging, git workflow, code review, development automation, and auto-installs all baleen-plugins", - "author": { - "name": "baleen37", - "email": "git@baleen.me" - } -} diff --git a/plugins/me/README.md b/plugins/me/README.md deleted file mode 100644 index 4cb48e72..00000000 --- a/plugins/me/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Me - -Personal Claude Code configuration - TDD, systematic debugging, git workflow, code review, and development automation. - -## Components - -### Commands (10) - -- **brainstorm**: Brainstorming and planning -- **create-pr**: Commit, push, and create/update pull requests -- **debug**: Systematic debugging process -- **orchestrate**: Execute sequential agent workflows -- **refactor-clean**: Code refactoring and cleanup -- **research**: Web research with citations -- **sdd**: Subagent-driven development approach -- **spawn**: Create new git worktree for isolated feature work -- **tdd**: Test-driven development workflow -- **verify**: Comprehensive codebase verification - -### Agents - -- **code-reviewer**: Review code against plans and standards - -### Skills (3) - -- **ci-troubleshooting**: Systematic CI debugging approach -- **spawn-worktree**: Isolated feature work with git worktrees -- **test-driven-development**: TDD methodology and workflow - -## Installation - -```bash -/plugin marketplace add /path/to/claude-plugins -/plugin install me@claude-plugins -``` - -## Usage - -This plugin provides a comprehensive set of tools for: - -- Git workflow automation (commit, PR, worktree management) -- CI/CD troubleshooting and debugging -- Nix flakes development environment setup -- Code review and quality assurance -- Handoff between sessions - -## Philosophy - -Following strict TDD and systematic debugging practices. All features and bugfixes -follow test-driven development, and root cause analysis is mandatory for any issue -resolution. - -## Repository - -Part of the [dotfiles](https://github.com/baleen37/dotfiles) project - Nix -flakes-based reproducible development environments for macOS and NixOS. diff --git a/plugins/me/bun.lock b/plugins/me/bun.lock deleted file mode 100644 index ac992904..00000000 --- a/plugins/me/bun.lock +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "@baleen/me", - "devDependencies": { - "@baleen/bats-helpers": "file:../../packages/bats-helpers", - }, - }, - }, - "packages": { - "@baleen/bats-helpers": ["@baleen/bats-helpers@file:../../packages/bats-helpers", {}], - } -} diff --git a/plugins/me/hooks/hooks.json b/plugins/me/hooks/hooks.json deleted file mode 100644 index 8d84b5ce..00000000 --- a/plugins/me/hooks/hooks.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../schemas/hooks-schema.json", - "description": "Me plugin hooks", - "hooks": { - "PreToolUse": [] - } -} diff --git a/plugins/me/package.json b/plugins/me/package.json deleted file mode 100644 index 869fca6e..00000000 --- a/plugins/me/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@baleen/me", - "version": "5.0.0", - "description": "Personal Claude Code configuration - TDD, systematic debugging, git workflow, code review, development automation, and auto-installs all baleen-plugins", - "author": { - "name": "baleen37", - "email": "git@baleen.me" - }, - "scripts": { - "test": "bats tests/" - }, - "devDependencies": { - "@baleen/bats-helpers": "file:../../packages/bats-helpers" - } -} diff --git a/plugins/me/scripts/check-conflicts.sh b/plugins/me/scripts/check-conflicts.sh deleted file mode 100755 index d0df5264..00000000 --- a/plugins/me/scripts/check-conflicts.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# check-conflicts.sh - Check for merge conflicts between current branch and base -# -# Usage: check-conflicts.sh -# -# Exit codes: -# 0 - No conflicts (clean merge) -# 1 - Conflicts detected (manual resolution required) -# 2 - Error (missing base branch, git errors, etc.) - -# Color codes for output -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -NC='\033[0m' # No Color - -# Check arguments -if [[ $# -ne 1 ]]; then - echo -e "${RED}ERROR: Missing base branch argument${NC}" >&2 - echo "Usage: $0 " >&2 - exit 2 -fi - -BASE="$1" - -# Verify we're in a git repository -if ! git rev-parse --git-dir >/dev/null 2>&1; then - echo -e "${RED}ERROR: Not in a git repository${NC}" >&2 - exit 2 -fi - -# Fetch latest base branch -echo "Fetching origin/$BASE..." -if ! git fetch origin "$BASE" 2>/dev/null; then - echo -e "${RED}ERROR: Failed to fetch origin/$BASE${NC}" >&2 - echo " - Check if remote 'origin' exists: git remote -v" >&2 - echo " - Check if branch '$BASE' exists on remote" >&2 - exit 2 -fi - -# Get merge base -MERGE_BASE=$(git merge-base HEAD "origin/$BASE" 2>/dev/null) -if [[ -z "$MERGE_BASE" ]]; then - echo -e "${RED}ERROR: Cannot find common ancestor with origin/$BASE${NC}" >&2 - exit 2 -fi - -# Check for merge conflicts using merge-tree -echo "Checking for conflicts with origin/$BASE..." -MERGE_OUTPUT=$(git merge-tree "$MERGE_BASE" HEAD "origin/$BASE" 2>&1) -MERGE_EXIT=$? - -if [[ $MERGE_EXIT -ne 0 ]]; then - # Conflicts detected - echo -e "${YELLOW}WARNING: Conflicts detected with origin/$BASE${NC}" >&2 - echo "" >&2 - - # Try to detect whitespace-only conflicts - DIFF_OUTPUT=$(git diff "origin/$BASE"...HEAD 2>/dev/null || echo "") - - if echo "$DIFF_OUTPUT" | grep -q "^[-+]\s*$"; then - echo -e "${YELLOW}Note: Some conflicts may be whitespace-only${NC}" >&2 - echo " - Consider: git merge origin/$BASE --strategy-option=ignore-space-change" >&2 - fi - - # Show conflict summary - echo -e "${RED}Conflicts require manual resolution:${NC}" >&2 - echo " 1. git fetch origin $BASE" >&2 - echo " 2. git merge origin/$BASE" >&2 - echo " 3. Resolve conflicts in affected files" >&2 - echo " 4. git add " >&2 - echo " 5. git commit" >&2 - echo "" >&2 - - # Try to extract conflicting files from merge-tree output - if echo "$MERGE_OUTPUT" | grep -q "CONFLICT"; then - echo -e "${RED}Files with conflicts:${NC}" >&2 - echo "$MERGE_OUTPUT" | grep "CONFLICT" | sed 's/^/ - /' >&2 - fi - - exit 1 -fi - -# No conflicts -echo -e "${GREEN}✓ No conflicts detected${NC}" -echo " - Current branch merges cleanly with origin/$BASE" -exit 0 diff --git a/plugins/me/scripts/verify-pr-status.sh b/plugins/me/scripts/verify-pr-status.sh deleted file mode 100755 index a1e324a3..00000000 --- a/plugins/me/scripts/verify-pr-status.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# PR Status Verification with Retry Logic -# Usage: verify-pr-status.sh -# -# Exit codes: -# 0 - PR is merge-ready (CLEAN + CI passed) -# 1 - Error (conflicts, CI failures, max retries exceeded) -# 2 - Pending (CI still running, BLOCKED/UNSTABLE status) - -BASE="${1:-}" -if [[ -z "$BASE" ]]; then - echo "ERROR: Base branch required" >&2 - echo "Usage: $0 " >&2 - exit 1 -fi - -MAX_RETRIES=3 -RETRY_COUNT=0 - -# Get initial status -PR_URL=$(gh pr view --json url -q .url) -PR_STATUS=$(gh pr view --json mergeable,mergeStateStatus) -MERGEABLE=$(echo "$PR_STATUS" | jq -r .mergeable) -STATE=$(echo "$PR_STATUS" | jq -r .mergeStateStatus) - -while [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do - case "$STATE" in - CLEAN) - # CRITICAL: Check CI before declaring merge-ready - CHECKS=$(gh pr view --json statusCheckRollup -q '.statusCheckRollup') - - PENDING_REQUIRED=$(echo "$CHECKS" | jq '[.[] | select(.isRequired==true and (.state=="PENDING" or .state=="IN_PROGRESS"))] | length') - FAILED_REQUIRED=$(echo "$CHECKS" | jq '[.[] | select(.isRequired==true and (.state=="FAILURE" or .state=="ERROR"))] | length') - - if [[ $FAILED_REQUIRED -gt 0 ]]; then - echo "" - echo "✗ Required CI checks failed" - echo "$CHECKS" | jq -r '.[] | select(.isRequired==true and (.state=="FAILURE" or .state=="ERROR")) | " - ❌ \(.context): \(.state)"' - echo "" - echo "Fix CI failures before merge" - echo "Monitor: gh pr checks $PR_URL" - exit 1 - fi - - if [[ $PENDING_REQUIRED -gt 0 ]]; then - echo "" - echo "⚠ PR status: CLEAN but required CI checks still running" - echo "$CHECKS" | jq -r '.[] | select(.isRequired==true and (.state=="PENDING" or .state=="IN_PROGRESS")) | " - ⏳ \(.context): \(.state)"' - echo "" - echo "Cannot confirm merge-ready until CI completes" - echo "Monitor: gh pr checks $PR_URL" - echo "URL: $PR_URL" - exit 2 - fi - - # All checks passed - echo "" - echo "✓ PR is merge-ready" - echo " - Status: CLEAN" - echo " - Required checks: Passed" - echo " - URL: $PR_URL" - exit 0 - ;; - - BEHIND) - RETRY_COUNT=$((RETRY_COUNT + 1)) - echo "" - echo "⟳ Branch is behind $BASE (attempt $RETRY_COUNT/$MAX_RETRIES)" - - # Update branch - git merge origin/"$BASE" --no-edit - - if [[ $? -ne 0 ]]; then - echo "" - echo "✗ Merge failed - conflicts detected" - echo "" - echo "Files with conflicts:" - git diff --name-only --diff-filter=U | sed 's/^/ - /' - echo "" - echo "Resolution steps:" - echo " 1. Resolve conflicts in listed files" - echo " 2. git add " - echo " 3. git commit" - echo " 4. git push" - echo " 5. Re-run this workflow" - exit 1 - fi - - git push - - # REQUIRED: Re-check status after update - echo " Verifying updated status..." - PR_STATUS=$(gh pr view --json mergeable,mergeStateStatus) - MERGEABLE=$(echo "$PR_STATUS" | jq -r .mergeable) - STATE=$(echo "$PR_STATUS" | jq -r .mergeStateStatus) - ;; - - DIRTY) - echo "" - echo "✗ PR has conflicts" - echo "" - echo "Files with conflicts:" - git diff --name-only --diff-filter=U | sed 's/^/ - /' - echo "" - echo "Resolution steps:" - echo " 1. git fetch origin $BASE" - echo " 2. git merge origin/$BASE" - echo " 3. Resolve conflicts in listed files" - echo " 4. git add " - echo " 5. git commit -m 'chore: resolve merge conflicts'" - echo " 6. git push" - echo "" - echo "Then check status: gh pr view" - exit 1 - ;; - - BLOCKED|UNSTABLE) - echo "" - echo "⚠ PR status: $STATE" - echo " - Mergeable: $MERGEABLE" - echo " - This may resolve automatically as CI completes" - echo " - Check status: gh pr view" - echo " - URL: $PR_URL" - exit 2 - ;; - - *) - echo "" - echo "⚠ Unknown status: $STATE" - echo " - Mergeable: $MERGEABLE" - echo " - Check manually: $PR_URL" - exit 1 - ;; - esac -done - -# Max retries exceeded -echo "" -echo "✗ PR still BEHIND after $MAX_RETRIES attempts" -echo " - Base branch is advancing faster than updates" -echo " - Manual intervention required" -echo " - Try: git fetch origin $BASE && git merge origin/$BASE && git push" -echo " - URL: $PR_URL" -exit 1 diff --git a/plugins/me/skills/README.md b/plugins/me/skills/README.md deleted file mode 100644 index ab275207..00000000 --- a/plugins/me/skills/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# Claude Code Skills - -This directory contains reusable workflow skills that enforce best practices. - -## Structure - -Each skill is a self-contained directory with `SKILL.md`: - -```text -skills/ -├── SKILL.md # Skill definition (mandatory) -├── scripts/ # Bash/shell scripts (optional) -│ └── *.sh # Executable helper scripts -├── templates/ # Template files (optional) -│ └── *.{md,nix,yaml} # Reusable templates -└── tools/ # Node.js / other tools (optional) - └── *.js # Tool scripts -``` - -## Naming Conventions - -- **Directory**: `kebab-case` (e.g., `creating-pull-requests`) -- **Skill file**: `SKILL.md` (always uppercase, not skill.md) -- **Scripts**: `kebab-case.sh` (e.g., `pr-check.sh`) - -## Available Skills - -| Skill | Purpose | When to Use | -|-------|---------|-------------| -| `ci-troubleshooting` | Systematic CI debugging | CI failures | -| `setup-precommit-and-ci` | Pre-commit hooks & CI setup | New project setup | -| `nix-direnv-setup` | Direnv for Nix flakes | Nix flake projects | -| `using-git-worktrees` | Isolated branch work | Feature development | -| `video-debugging` | Video playback issues | Media debugging | -| `writing-claude-commands` | Creating Claude Code commands | Adding /commands | - -## Skill Anatomy - -A well-structured skill: - -```markdown ---- -name: skill-name -description: Use when... - clear trigger condition ---- - -# Skill Name - -## Overview -Brief description of what the skill does. - -## When to Use -Clear criteria for when this skill applies. - -## Implementation -Step-by-step instructions for the workflow. - -## Examples -Concrete usage examples. -``` - -## Best Practices - -1. **One skill, one purpose** - Don't combine unrelated workflows -2. **Clear trigger conditions** - "Use when X happens" -3. **Enforced methodology** - Skills should guide, not just inform -4. **Idempotent** - Can be run multiple times safely -5. **Self-documenting** - SKILL.md is the single source of truth - -## Creating New Skills - -1. Create directory: `mkdir skills/your-skill` -2. Create `SKILL.md` with proper frontmatter -3. Add optional `scripts/`, `templates/`, or `tools/` -4. Test locally before committing -5. Document in this README - -See `writing-claude-commands` skill for detailed guidance. diff --git a/plugins/me/skills/remembering-conversations/SKILL.md b/plugins/me/skills/remembering-conversations/SKILL.md deleted file mode 100644 index 9d154dc6..00000000 --- a/plugins/me/skills/remembering-conversations/SKILL.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: remembering-conversations -description: This skill should be used when the user asks "how should I...", "what's the best approach...", "do you remember...", "why did we...", mentions "last time", "before", "we discussed", or when stuck after investigation. Searches conversation history using claude-mem 3-layer workflow. -version: 0.3.0 ---- - -# Remembering Conversations - -Search conversation history to find past decisions, patterns, and failed approaches before reinventing. - -## Core Principle - -**Search before reinventing.** Searching costs nothing; reinventing or repeating mistakes costs everything. - -## When to Use - -**After understanding the task:** - -- User asks "how should I..." or "what's the best approach..." -- After exploring codebase, need to make architectural decisions - -**When stuck:** - -- Investigated a problem, can't find solution -- Need to follow unfamiliar workflow - -**Historical signals:** - -- User says "last time", "before", "we discussed" -- User asks "why did we...", "do you remember..." - -**Don't search first:** - -- For current codebase structure (use Grep/Read) -- For info in current conversation -- Before understanding the task - -## 3-Layer Workflow - -**ALWAYS follow this workflow to save 10x tokens:** - -### 1. Search for Index - -`mcp__plugin_claude-mem_mcp-search__search` - -- Get lightweight index with observation IDs -- Params: `query`, `limit`, `dateStart`, `dateEnd` - -### 2. Get Context - -`mcp__plugin_claude-mem_mcp-search__timeline` - -- Get context around interesting results -- Params: `query` or `anchor`, `depth_before`, `depth_after` - -### 3. Fetch Details - -`mcp__plugin_claude-mem_mcp-search__get_observations` - -- Fetch ONLY filtered IDs from steps 1-2 -- Params: `ids` (required array) - -**NEVER fetch full details without filtering first.** - -## Strategy - -1. Search broad: `{ query: "authentication" }` -2. Review index results -3. Get timeline context for interesting IDs -4. Fetch full details for most relevant observations -5. Synthesize: key insights, apply to context, recommend approach - -## Extract - -- Past decisions and rationale -- Failed approaches and why -- Successful patterns -- Gotchas and edge cases diff --git a/plugins/ralph-loop/.claude-plugin/marketplace.json b/plugins/ralph-loop/.claude-plugin/marketplace.json deleted file mode 100644 index 11426a08..00000000 --- a/plugins/ralph-loop/.claude-plugin/marketplace.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "ralph-loop", - "version": "5.7.1", - "description": "Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique. Run Claude in a while-true loop with the same prompt until task completion.", - "author": { - "name": "Anthropic", - "email": "support@anthropic.com" - } -} diff --git a/plugins/ralph-loop/.claude-plugin/plugin.json b/plugins/ralph-loop/.claude-plugin/plugin.json deleted file mode 100644 index 85ddb66b..00000000 --- a/plugins/ralph-loop/.claude-plugin/plugin.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "ralph-loop", - "version": "5.23.3", - "description": "Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique. Run Claude in a while-true loop with the same prompt until task completion.", - "author": { - "name": "Anthropic", - "email": "support@anthropic.com" - } -} diff --git a/plugins/ralph-loop/README.md b/plugins/ralph-loop/README.md deleted file mode 100644 index c725a65a..00000000 --- a/plugins/ralph-loop/README.md +++ /dev/null @@ -1,670 +0,0 @@ -# Ralph Wiggum Plugin - -Implementation of the Ralph Wiggum technique for iterative, self-referential AI development loops in Claude Code. - -## What is Ralph? - -Ralph is a development methodology based on continuous AI agent loops. As Geoffrey -Huntley describes it: **"Ralph is a Bash loop"** - a simple `while true` that repeatedly -feeds an AI agent a prompt file, allowing it to iteratively improve its work until -completion. - -The technique is named after Ralph Wiggum from The Simpsons, embodying the philosophy of persistent iteration despite setbacks. - -### Core Concept - -This plugin implements Ralph using a **bash loop + fresh instances** approach: - -```bash -# You run ONCE: -/ralph-init "Your task description" - -# Then: -/ralph-loop - -# Behind the scenes, a bash loop runs: -while true; do - claude --print PROMPT.md -done -``` - -Each iteration spawns a **fresh Claude instance** that receives the SAME prompt. -The "self-referential" aspect comes from Claude seeing its own previous work -in the files and git history, not from feeding output back as input. - -This creates a **self-referential feedback loop** where: - -- The prompt never changes between iterations -- Claude's previous work persists in files -- Each iteration sees modified files and git history -- Claude autonomously improves by reading its own past work in files -- Fresh instances provide clean state each iteration (no session pollution) - -## Quick Start - -```bash -# 1. Initialize a PRD with user stories -/ralph-init "Build a REST API for todos. Requirements: CRUD operations, input validation, tests." - -# 2. Start the loop (default 10 iterations) -/ralph-loop - -# Or specify max iterations -/ralph-loop 50 - -# 3. Monitor progress in another terminal -tail -f .ralph/progress.txt - -# 4. Cancel if needed -/cancel-ralph -``` - -Claude will: - -- Implement one user story per iteration -- Run tests and quality checks -- Commit work if tests pass -- Continue until all stories pass or max iterations reached - -## Architecture - -### The .ralph Directory - -Ralph uses a `.ralph/` directory for state management: - -```text -.ralph/ -├── prd.json # Product Requirements Document with user stories -├── progress.txt # Progress log with learnings from each iteration -└── ralph.pid # Process ID of active loop (exists only while running) -``` - -### PRD Format - -The `prd.json` file contains your task broken into user stories: - -```json -{ - "project": "my-project", - "branchName": "ralph/add-todo-api", - "description": "Build a REST API for todos", - "userStories": [ - { - "id": "US-001", - "title": "Create todo model", - "description": "As a developer, I want a todo data model so that I can store todos.", - "acceptanceCriteria": [ - "Todo model with id, title, completed fields", - "Tests pass", - "Typecheck passes" - ], - "priority": 1, - "passes": false - } - ] -} -``` - -### Iteration Workflow - -Each iteration follows this pattern: - -1. Fresh Claude instance starts -2. Reads `.ralph/prd.json` to find user stories -3. Reads `.ralph/progress.txt` for previous learnings -4. Finds the highest-priority story where `passes` is `false` -5. Implements ONLY that one story -6. Runs tests and quality checks (typecheck, lint, test) -7. If checks pass: - - Commits with message: `feat: [STORY_ID] - Story Title` - - Updates `.ralph/prd.json`: sets `passes: true` for this story - - Appends learnings to `.ralph/progress.txt` -8. If checks fail: - - Appends what went wrong to `.ralph/progress.txt` - - Does NOT mark the story as passing -9. Checks if ALL stories have `passes: true`: - - If yes: outputs `COMPLETE` (loop exits) - - If no: stops (next iteration picks up the next story) - -### Prompt Template - -The `scripts/prompt.md` template is used for each iteration: - -```markdown -You are executing iteration {{ITERATION}} of {{MAX}} in a Ralph loop. - -## Instructions - -1. Read `.ralph/prd.json` to find user stories -2. Read `.ralph/progress.txt` for learnings from previous iterations -3. Find the highest-priority story where `passes` is `false` -4. Implement ONLY that one story -5. Run tests and quality checks (typecheck, lint, test) -6. If checks pass: - - Commit with message: `feat: [STORY_ID] - Story Title` - - Update `.ralph/prd.json`: set `passes: true` for this story - - Append what you learned to `.ralph/progress.txt` -7. If checks fail: - - Append what went wrong to `.ralph/progress.txt` - - Do NOT mark the story as passing -8. After processing one story, check if ALL stories have `passes: true` - - If yes: output exactly `COMPLETE` - - If no: stop (next iteration will pick up the next story) - -## Rules - -- Implement ONE story per iteration. Do not try to do multiple stories. -- Always run tests before marking a story as passing. -- Never mark a story as passing if tests fail. -- Never delete or skip tests. -- Write useful learnings to progress.txt — the next iteration depends on them. -``` - -## Commands - -### /ralph-init "description" - -Initialize a PRD (Product Requirements Document) from a task description. - -**Usage:** - -```bash -/ralph-init "Build a REST API for todos with CRUD operations, input validation, and tests." -``` - -**What it does:** - -- Analyzes the task description -- Breaks it into user stories (completable in one iteration) -- Creates `.ralph/prd.json` with stories and acceptance criteria -- Creates `.ralph/progress.txt` for tracking -- Reports the story breakdown - -**Story guidelines:** - -- **Right-sized**: Each story should be completable in one focused session -- **Verifiable**: Include "Tests pass" and "Typecheck passes" in acceptance criteria -- **Independent**: Minimize dependencies between stories -- **Priority order**: Foundational work (DB, types, models) before features, features before UI - -### /ralph-loop [max-iterations] - -Start the Ralph loop with a bash script. - -**Usage:** - -```bash -# Default: 10 iterations -/ralph-loop - -# Custom max iterations -/ralph-loop 20 -``` - -**What it does:** - -- Reads `.ralph/prd.json` for user stories -- Spawns fresh `claude --print` instances in a bash loop -- Each instance implements one story, runs tests, commits if passing -- Loop exits when all stories pass or max iterations reached -- Progress tracked in `.ralph/progress.txt` - -**Monitoring progress:** - -```bash -tail -f .ralph/progress.txt -``` - -### /cancel-ralph - -Cancel an active Ralph loop. - -**Usage:** - -```bash -/cancel-ralph -``` - -**What it does:** - -- Checks for `.ralph/ralph.pid` (loop process ID) -- Kills the bash loop process -- Removes PID file -- Reports cancellation - -## Prompt Writing Best Practices - -### 1. Clear Task Description - -❌ Bad: "Build a todo API and make it good." - -✅ Good: - -```bash -/ralph-init "Build a REST API for todos. Requirements: -- CRUD operations (create, read, update, delete) -- Input validation (title required, max 100 chars) -- Unit tests for all endpoints -- Integration tests -- API documentation -- Type safety with TypeScript -``` - -### 2. Right-Sized Stories - -When `/ralph-init` creates stories, it should break the task into manageable pieces: - -❌ Too big: "Build the entire e-commerce platform" - -✅ Right-sized: - -- US-001: Setup project structure and dependencies -- US-002: Create product data model -- US-003: Implement product list endpoint -- US-004: Add product creation with validation -- US-005: Write tests for product endpoints - -### 3. Include Testing Stories - -Make testing explicit: - -✅ Good breakdown: - -- US-001: Create user model -- US-002: Write unit tests for user model -- US-003: Implement user service -- US-004: Write integration tests for user service -- US-005: Create user API endpoints -- US-006: Write API tests for endpoints - -### 4. Set Reasonable Iteration Limits - -Always use max-iterations as a safety net: - -```bash -# Recommended: Estimate based on story count -# 5 stories × 2-3 iterations per story = 10-15 iterations -/ralph-loop 15 - -# For larger tasks: 10 stories × 3 iterations = 30 iterations -/ralph-loop 30 -``` - -### 5. Verifiable Acceptance Criteria - -Each story should have clear pass/fail criteria: - -✅ Good acceptance criteria: - -- "Todo model with id, title, completed fields defined" -- "POST /todos creates a todo and returns 201" -- "GET /todos returns array of todos" -- "Tests pass (npm test)" -- "Typecheck passes (npm run typecheck)" - -## Complete Example - -Let's walk through a complete session building a simple todo API feature. - -### Step 1: Initialize the PRD - -```bash -/ralph-init "Add a simple todo API with POST /todos to create todos and GET /todos to list them. Use TypeScript, Express, and include tests." -``` - -Claude breaks this into stories and creates `.ralph/prd.json`: - -```json -{ - "project": "my-api", - "branchName": "ralph/add-todo-api", - "description": "Add a simple todo API with POST /todos to create todos and GET /todos to list them. Use TypeScript, Express, and include tests.", - "userStories": [ - { - "id": "US-001", - "title": "Setup Express server with TypeScript", - "description": "As a developer, I want an Express server with TypeScript configured so that I can build API endpoints.", - "acceptanceCriteria": [ - "Express server listens on port 3000", - "TypeScript configured", - "ts-node for development", - "Tests pass", - "Typecheck passes" - ], - "priority": 1, - "passes": false - }, - { - "id": "US-002", - "title": "Create todo model and interface", - "description": "As a developer, I want a todo model so that I can define the structure of todo data.", - "acceptanceCriteria": [ - "Todo interface with id, title, completed fields", - "Typecheck passes" - ], - "priority": 2, - "passes": false - }, - { - "id": "US-003", - "title": "Implement POST /todos endpoint", - "description": "As a user, I want to create todos so that I can track tasks.", - "acceptanceCriteria": [ - "POST /todos accepts {title: string}", - "Returns created todo with id and completed: false", - "Input validation (title required)", - "Tests pass", - "Typecheck passes" - ], - "priority": 3, - "passes": false - }, - { - "id": "US-004", - "title": "Implement GET /todos endpoint", - "description": "As a user, I want to list all todos so that I can see my tasks.", - "acceptanceCriteria": [ - "GET /todos returns array of todos", - "Tests pass", - "Typecheck passes" - ], - "priority": 4, - "passes": false - } - ] -} -``` - -### Step 2: Start the Loop - -```bash -/ralph-loop 20 -``` - -### Step 3: Watch Progress - -In another terminal: - -```bash -tail -f .ralph/progress.txt -``` - -### Step 4: Iteration Walkthrough - -#### Iteration 1 - US-001: Setup Express server - -**What happens:** - -1. Fresh Claude instance starts -2. Reads `.ralph/prd.json` - finds US-001 is highest priority with `passes: false` -3. Reads `.ralph/progress.txt` - empty (first iteration) -4. Implements Express server setup -5. Runs tests: **FAILS** - test file not created yet - -**Progress.txt after iteration 1:** - -```text -# Ralph Progress Log -Started: 2026-02-08T12:00:00Z - -## Codebase Patterns -(No patterns discovered yet) - -## Iteration 1 - US-001: Setup Express server with TypeScript - -### Attempted -- Created src/server.ts with Express app -- Added tsconfig.json -- Added package.json with dependencies - -### Issues -- Tests failed: No test file found -- Need to add test framework (Jest or similar) - -### Next Steps -- Add test framework setup -- Create basic test file - ---- -``` - -**prd.json after iteration 1:** US-001 still has `"passes": false` because tests failed. - -#### Iteration 2 - US-001: Setup Express server (retry) - -**What happens:** - -1. Fresh Claude instance starts -2. Reads `.ralph/prd.json` - US-001 still has `passes: false` -3. Reads `.ralph/progress.txt` - sees that test framework was missing -4. Adds Jest configuration and creates test file -5. Runs tests: **PASS** -6. Commits: `feat: US-001 - Setup Express server with TypeScript` -7. Updates `prd.json`: US-001 now has `"passes": true` -8. Appends to `progress.txt` - -**Progress.txt after iteration 2:** - -```text -## Iteration 2 - US-001: Setup Express server with TypeScript - -### Successful -- Added Jest configuration -- Created src/server.test.ts with basic test -- All tests passing -- Typecheck passing - -### Committed -- feat: US-001 - Setup Express server with TypeScript - -### Learnings -- Project uses Jest for testing -- Test command: npm test -- Typecheck command: npm run typecheck - ---- -``` - -**prd.json after iteration 2:** US-001 now has `"passes": true`. - -#### Iteration 3 - US-002: Create todo model - -**What happens:** - -1. Fresh Claude instance starts -2. Reads `.ralph/prd.json` - US-002 is next highest priority with `passes: false` -3. Reads `.ralph/progress.txt` - learns test commands and patterns -4. Creates Todo interface -5. Runs typecheck: **PASS** -6. Commits: `feat: US-002 - Create todo model and interface` -7. Updates `prd.json`: US-002 now has `"passes": true` - -**Progress.txt after iteration 3:** - -```text -## Iteration 3 - US-002: Create todo model and interface - -### Successful -- Created src/models/Todo.ts with interface -- Typecheck passing - -### Committed -- feat: US-002 - Create todo model and interface - ---- -``` - -#### Iteration 4 - US-003: Implement POST /todos - -**What happens:** - -1. Fresh Claude instance starts -2. Reads `.ralph/prd.json` - US-003 is next with `passes: false` -3. Reads `.ralph/progress.txt` - learns project structure -4. Implements POST /todos endpoint -5. Runs tests: **FAILS** - missing input validation - -**Progress.txt after iteration 4:** - -```text -## Iteration 4 - US-003: Implement POST /todos endpoint - -### Attempted -- Created POST /todos endpoint in src/routes/todos.ts -- Added in-memory todo storage - -### Issues -- Tests failing: Missing input validation for title field -- Need to validate title is not empty -- Need to validate title max length - ---- -``` - -#### Iteration 5 - US-003: Fix POST /todos validation - -**What happens:** - -1. Fresh Claude instance starts -2. Reads `.ralph/prd.json` - US-003 still has `passes: false` -3. Reads `.ralph/progress.txt` - sees validation issues -4. Adds input validation middleware -5. Runs tests: **PASS** -6. Commits: `feat: US-003 - Implement POST /todos endpoint` -7. Updates `prd.json`: US-003 now has `"passes": true` - -#### Iteration 6 - US-004: Implement GET /todos - -**What happens:** - -1. Fresh Claude instance starts -2. Reads `.ralph/prd.json` - US-004 is last story with `passes: false` -3. Reads `.ralph/progress.txt` - learns patterns -4. Implements GET /todos endpoint -5. Runs tests: **PASS** -6. Commits: `feat: US-004 - Implement GET /todos endpoint` -7. Updates `prd.json`: US-004 now has `"passes": true` -8. Checks if ALL stories pass: **YES** -9. Outputs: `COMPLETE` - -**Loop exits successfully.** - -### Final State - -**prd.json:** All stories have `"passes": true` - -**Git history:** - -```text -d69aec5 feat: US-004 - Implement GET /todos endpoint -c8f4b2a feat: US-003 - Implement POST /todos endpoint -a3e7d1c feat: US-002 - Create todo model and interface -7b2c9e0 feat: US-001 - Setup Express server with TypeScript -``` - -**progress.txt:** Complete log of all iterations with learnings - -### Key Observations - -**When a story fails tests:** - -- Story remains marked as `passes: false` -- Next iteration retries the SAME story -- Progress.txt captures what went wrong -- Each iteration builds on previous work - -**When a story passes tests:** - -- Story is marked as `passes: true` -- Work is committed -- Next iteration moves to the next story -- Progress.txt captures what was learned - -**Fresh instance benefits:** - -- Each iteration starts with clean state -- No conversation context pollution -- Progress.txt provides continuity -- Failures are isolated to single iterations - -## Philosophy - -Ralph embodies several key principles: - -### 1. Iteration > Perfection - -Don't aim for perfect on first try. Let the loop refine the work. Each iteration -builds on the previous one, incrementally improving toward the goal. - -### 2. Failures Are Data - -"Deterministically bad" means failures are predictable and informative. Use them -to tune prompts. The `progress.txt` file captures learnings from each failure, -informing subsequent iterations. - -### 3. Operator Skill Matters - -Success depends on writing good task descriptions and story breakdowns, not just -having a good model. The quality of your `/ralph-init` prompt determines the -quality of the results. - -### 4. Persistence Wins - -Keep trying until success. The loop handles retry logic automatically. If a story -fails tests, the next iteration sees the failure in `progress.txt` and can fix it. - -### 5. Fresh State Benefits - -The new architecture uses fresh Claude instances for each iteration: - -- **Clean state**: No session context accumulation -- **Natural exit**: Each iteration completes independently -- **Better error isolation**: Failures don't pollute subsequent iterations -- **Easier debugging**: Each iteration is a discrete unit of work - -## When to Use Ralph - -**Good for:** - -- Well-defined tasks with clear success criteria -- Tasks requiring iteration and refinement (e.g., getting tests to pass) -- Test-driven development workflows -- Greenfield projects where you can walk away -- Features that can be broken into small stories -- Tasks with automatic verification (tests, linters, typecheck) - -**Not good for:** - -- Tasks requiring human judgment or design decisions -- One-shot operations -- Tasks with unclear success criteria -- Production debugging (use targeted debugging instead) -- Tasks that need conversation/clarification - -## Real-World Results - -- Successfully generated 6 repositories overnight in Y Combinator hackathon testing -- One $50k contract completed for $297 in API costs -- Created entire programming language ("cursed") over 3 months using this approach - -## Completion Detection - -The loop completes when: - -1. **All stories pass**: Every user story in `prd.json` has `passes: true` -2. **Completion promise**: Claude outputs `COMPLETE` -3. **Max iterations**: The loop reaches the iteration limit (exits with error) - -The loop script checks for the completion promise in Claude's output after each -iteration. When found, it exits cleanly. If max iterations is reached without -completion, the script exits with an error. - -## Learn More - -- Original technique: -- Inspiration: -- Ralph Orchestrator: - -## For Help - -Run `/help` in Claude Code for detailed command reference and examples. diff --git a/plugins/suggest-compacting/.claude-plugin/plugin.json b/plugins/suggest-compacting/.claude-plugin/plugin.json deleted file mode 100644 index c2095519..00000000 --- a/plugins/suggest-compacting/.claude-plugin/plugin.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "suggest-compacting", - "version": "5.23.3", - "description": "Suggests when to manually compact context during long sessions", - "author": { - "name": "baleen37", - "email": "git@baleen.me" - } -} diff --git a/plugins/suggest-compacting/.gitignore b/plugins/suggest-compacting/.gitignore deleted file mode 100644 index a83ca16f..00000000 --- a/plugins/suggest-compacting/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Dependencies - -# Bun -bun.lockb diff --git a/plugins/suggest-compacting/README.md b/plugins/suggest-compacting/README.md deleted file mode 100644 index ad98783c..00000000 --- a/plugins/suggest-compacting/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Suggest Compacting - -Suggests when to manually compact context during long Claude Code sessions. - -## Overview - -Suggest Compacting monitors your editing activity and suggests when to manually run `/compact`. Unlike forced auto-compaction that interrupts at arbitrary moments, this plugin preserves your workflow by providing non-intrusive suggestions at logical intervals. - -**Key distinction**: The plugin suggests; you decide when to compact. - -## How It Works - -The hook tracks Edit/Write tool calls per session and suggests when to compact: -- First suggestion at 50 tool calls (configurable via `COMPACT_THRESHOLD`) -- Subsequent suggestions every 25 calls thereafter -- Non-blocking stderr messages don't interrupt workflows - -## Usage - -Install via the baleen-plugins marketplace: - -```bash -/plugin install suggest-compacting@baleen-plugins -``` - -## Configuration - -Set a custom threshold via environment variable: - -```bash -export COMPACT_THRESHOLD=100 -``` - -Default: 50 tool calls - -## Messages - -When thresholds are reached, you'll see non-intrusive suggestions: - -``` -[SuggestCompacting] 50 tool calls reached - consider /compact if transitioning phases -[SuggestCompacting] 75 tool calls - good checkpoint for /compact if context is stale -``` - -## Use Cases - -Ideal for: -- Long debugging sessions (compact after finding root cause) -- Multi-phase features (compact after exploration, before implementation) -- Refactoring work (compact after each module) -- Research-to-code transitions (compact before writing code) - -## Why Suggestions Over Auto-Compaction? - -**Forced auto-compaction problems:** -- Happens at unpredictable points, often mid-task -- Loses critical context during active work -- Interrupts thought processes -- Difficult to resume after compaction - -**Suggest Compacting benefits:** -- You control when compaction occurs -- Preserve context through logical phases -- Choose natural breakpoints in your workflow -- Maintain continuity of work - -## Repository - -Part of the [dotfiles](https://github.com/baleen37/dotfiles) project - Nix flakes-based reproducible development environments for macOS and NixOS. diff --git a/plugins/suggest-compacting/bun.lock b/plugins/suggest-compacting/bun.lock deleted file mode 100644 index 3d102ae0..00000000 --- a/plugins/suggest-compacting/bun.lock +++ /dev/null @@ -1,83 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "suggest-compacting-plugin", - "devDependencies": { - "@types/node": "^20.11.5", - "tsx": "^4.7.1", - "typescript": "^5.3.3", - }, - }, - }, - "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - - "@types/node": ["@types/node@20.19.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA=="], - - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "get-tsconfig": ["get-tsconfig@4.13.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-vp8Cj/+9Q/ibZUrq1rhy8mCTQpCk31A3uu9wc1C50yAb3x2pFHOsGdAZQ7jD86ARayyxZUViYeIztW+GE8dcrg=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - } -} diff --git a/plugins/suggest-compacting/hooks/hooks.json b/plugins/suggest-compacting/hooks/hooks.json deleted file mode 100644 index 08f38482..00000000 --- a/plugins/suggest-compacting/hooks/hooks.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "../../schemas/hooks-schema.json", - "description": "Auto Compact hooks - suggests manual compaction at logical intervals", - "hooks": { - "SessionStart": [ - { - "matcher": ".*", - "hooks": [ - { - "type": "command", - "command": "bun ${CLAUDE_PLUGIN_ROOT}/dist/session-start.js" - } - ] - } - ], - "PreToolUse": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun ${CLAUDE_PLUGIN_ROOT}/dist/auto-compact.js" - } - ] - } - ] - } -} diff --git a/scripts/ralph/prompt.md b/scripts/ralph/prompt.md deleted file mode 100644 index 20b04204..00000000 --- a/scripts/ralph/prompt.md +++ /dev/null @@ -1,144 +0,0 @@ -# Ralph Agent Instructions - -You are executing iteration {{ITERATION}} of {{MAX}} in a Ralph loop. - -## Your Task - -1. Read the PRD at `.ralph/prd.json` to find user stories -2. Read the progress log at `.ralph/progress.txt` (check Codebase Patterns section first) -3. Read the guardrails at `.ralph/guardrails.md` (check Lessons Learned and Patterns Discovered sections) -4. Find the **highest priority** story where `status` is not `"done"` -5. When starting work on a story: - - Update `.ralph/prd.json`: set `status: "in_progress"` and - `startedAt: ""` for this story -6. Implement ONLY that one story -7. Run quality checks (e.g., typecheck, lint, test - use whatever your project requires) -8. Update CLAUDE.md files if you discover reusable patterns (see below) -9. If checks pass: - - Commit ALL changes with message: `feat: [Story ID] - [Story Title]` - - Update `.ralph/prd.json`: set `status: "done"`, - `completedAt: ""`, and `passes: true` for this story - - Append your progress to `.ralph/progress.txt` - -10. If checks fail: - - Append what went wrong to `.ralph/progress.txt` - - Do NOT mark the story as done (keep status as "in_progress" or revert to "open" if appropriate) - -11. After processing one story, check if ALL stories have `status: "done"` - - If yes: output exactly `COMPLETE` - - If no: stop (next iteration will pick up the next story) - -## Progress Report Format - -APPEND to .ralph/progress.txt (never replace, always append): - -```text -## [Date/Time] - [Story ID] - -- What was implemented -- Files changed - -**Learnings for future iterations:** -- Patterns discovered (e.g., "this codebase uses X for Y") -- Gotchas encountered (e.g., "don't forget to update Z when changing W") -- Useful context (e.g., "the evaluation panel is in component X") - ---- -``` - -The learnings section is critical - it helps future iterations avoid repeating mistakes and understand the codebase better. - -## Guardrails Update Format - -APPEND to .ralph/guardrails.md (never replace, always append) when you discover important lessons or patterns: - -```text -## [Date/Time] - -### Lessons Learned -- Example: Component X requires prop Y to render correctly -- Example: Tests fail if service Z is not mocked properly - -### Patterns Discovered -- Example: All API calls go through the service layer -- Example: State management uses pattern X for async operations - ---- -``` - -Only add entries to guardrails.md when you discover **generalizable lessons** that would help -future iterations avoid common pitfalls or understand the codebase architecture better. - -## Consolidate Patterns - -If you discover a **reusable pattern** that future iterations should know, -add it to the `## Codebase Patterns` section at the TOP of .ralph/progress.txt -(create it if it doesn't exist). This section should consolidate the most -important learnings: - -```text -## Codebase Patterns - -- Example: Use `sql` template for aggregations -- Example: Always use `IF NOT EXISTS` for migrations -- Example: Export types from actions.ts for UI components -``` - -Only add patterns that are **general and reusable**, not story-specific details. - -## Update CLAUDE.md Files - -Before committing, check if any edited files have learnings worth preserving in nearby CLAUDE.md files: - -1. **Identify directories with edited files** - Look at which directories you modified -2. **Check for existing CLAUDE.md** - Look for CLAUDE.md in those directories or parent directories -3. **Add valuable learnings** - If you discovered something future developers/agents should know: - - API patterns or conventions specific to that module - - Gotchas or non-obvious requirements - - Dependencies between files - - Testing approaches for that area - - Configuration or environment requirements - -**Examples of good CLAUDE.md additions:** - -- "When modifying X, also update Y to keep them in sync" -- "This module uses pattern Z for all API calls" -- "Tests require the dev server running on PORT 3000" -- "Field names must match the template exactly" - -**Do NOT add:** - -- Story-specific implementation details -- Temporary debugging notes -- Information already in .ralph/progress.txt - -Only update CLAUDE.md if you have **genuinely reusable knowledge** that would help future work in that directory. - -## Quality Requirements - -- ALL commits must pass your project's quality checks (typecheck, lint, test) -- Do NOT commit broken code -- Keep changes focused and minimal -- Follow existing code patterns - -## Browser Testing (If Available) - -For any story that changes UI, verify it works in the browser if you have browser testing tools configured (e.g., via MCP): - -1. Navigate to the relevant page -2. Verify the UI changes work as expected -3. Take a screenshot if helpful for the progress log - -If no browser tools are available, note in your progress report that manual browser verification is needed. - -## Important - -- Work on ONE story per iteration. Do not try to do multiple stories. -- Always run tests before marking a story as passing. -- Never mark a story as passing if tests fail. -- Never delete or skip tests. -- Commit frequently. -- Keep CI green. -- Read the Codebase Patterns section in .ralph/progress.txt before starting. -- Read .ralph/guardrails.md before starting to avoid repeating past mistakes. -- Append lessons learned and patterns discovered to .ralph/guardrails.md after each story. diff --git a/scripts/ralph/ralph.sh b/scripts/ralph/ralph.sh deleted file mode 100755 index e5c3c8e2..00000000 --- a/scripts/ralph/ralph.sh +++ /dev/null @@ -1,353 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# === Configuration === -# Load user configuration if it exists -CONFIG_FILE=".agents/ralph/config.sh" -if [[ -f "$CONFIG_FILE" ]]; then - # shellcheck source=/dev/null - source "$CONFIG_FILE" -fi - -# Default STALE_SECONDS (24 hours) - can be overridden in config.sh -STALE_SECONDS="${STALE_SECONDS:-86400}" - -# Validate and set max iterations -if [[ -n "${1:-}" ]]; then - if ! [[ "$1" =~ ^[0-9]+$ ]]; then - echo "Error: MAX_ITERATIONS must be a positive integer (got: '$1')" >&2 - exit 1 - fi - MAX_ITERATIONS="$1" -else - MAX_ITERATIONS=10 -fi - -# Path constants -RALPH_DIR=".ralph" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PRD_FILE="$RALPH_DIR/prd.json" -PROGRESS_FILE="$RALPH_DIR/progress.txt" -PID_FILE="$RALPH_DIR/ralph.pid" -LOG_DIR="$RALPH_DIR/logs" -LAST_BRANCH_FILE="$RALPH_DIR/.last-branch" -ARCHIVE_DIR="$RALPH_DIR/archive" - -# Prompt template: check for user customization first, fall back to default -CUSTOM_PROMPT_TEMPLATE=".agents/ralph/prompts/loop.md" -if [[ -f "$CUSTOM_PROMPT_TEMPLATE" ]]; then - PROMPT_TEMPLATE="$CUSTOM_PROMPT_TEMPLATE" -else - PROMPT_TEMPLATE="$SCRIPT_DIR/prompt.md" -fi - -# Guardrails file stores lessons learned and patterns discovered during iterations -GUARDRAILS_FILE="$RALPH_DIR/guardrails.md" -# Activity log tracks iteration timestamps and completion status -ACTIVITY_LOG="$RALPH_DIR/activity.log" -# Errors log records iteration failures and error details -ERRORS_LOG="$RALPH_DIR/errors.log" - -# === Validation === -if [[ ! -f "$PRD_FILE" ]]; then - echo "Error: $PRD_FILE not found. Run /ralph-init first." >&2 - exit 1 -fi - -if [[ ! -f "$PROMPT_TEMPLATE" ]]; then - echo "Error: prompt template not found at $PROMPT_TEMPLATE" >&2 - exit 1 -fi - -# Validate prd.json is valid JSON and has required fields -validate_prd() { - if ! jq empty "$PRD_FILE" 2>/dev/null; then - echo "Error: prd.json is not valid JSON" >&2 - return 1 - fi - - local required_fields=("project" "userStories") - for field in "${required_fields[@]}"; do - if ! jq -e ".$field" "$PRD_FILE" >/dev/null 2>&1; then - echo "Error: prd.json missing required field: $field" >&2 - return 1 - fi - done - - if ! jq -e '.userStories | type == "array"' "$PRD_FILE" >/dev/null 2>&1; then - echo "Error: prd.json userStories must be an array" >&2 - return 1 - fi - - # Validate each user story has required fields - local story_required_fields=("id" "title" "description" "acceptanceCriteria" "priority" "status" "startedAt" "completedAt" "passes") - local story_count - story_count=$(jq -r '.userStories | length' "$PRD_FILE") - - for ((i=0; i/dev/null 2>&1; then - echo "Error: user story at index $i missing required field: $field" >&2 - return 1 - fi - done - - # Validate status enum values - local status - status=$(jq -r ".userStories[$i].status" "$PRD_FILE") - if [[ "$status" != "open" ]] && [[ "$status" != "in_progress" ]] && [[ "$status" != "done" ]]; then - echo "Error: user story at index $i has invalid status: '$status' (must be 'open', 'in_progress', or 'done')" >&2 - return 1 - fi - done -} - -if ! validate_prd; then - exit 1 -fi - -# === Helper functions === - -# Ensure we're on the correct branch -ensure_branch() { - local target_branch="$1" - [[ "$target_branch" == "null" || -z "$target_branch" ]] && return 0 - - local current_branch - current_branch=$(git branch --show-current) - [[ "$current_branch" == "$target_branch" ]] && return 0 - - if git show-ref --verify --quiet "refs/heads/$target_branch"; then - git checkout "$target_branch" - else - git checkout -b "$target_branch" - fi -} - -# Archive previous run data -archive_previous_run() { - local branch_to_archive="$1" - local archive_date - archive_date=$(date +"%Y-%m-%d") - - # Remove 'ralph/' prefix from branch name for archive directory - local feature_name="${branch_to_archive#ralph/}" - local archive_path="$ARCHIVE_DIR/${archive_date}-${feature_name}" - - # If archive directory already exists, append timestamp - if [[ -d "$archive_path" ]]; then - local timestamp - timestamp=$(date +"%H%M%S") - archive_path="${archive_path}-${timestamp}" - fi - - # Create archive directory - mkdir -p "$archive_path" - - # Archive state files - if [[ -f "$PRD_FILE" ]]; then - cp "$PRD_FILE" "$archive_path/" - fi - - if [[ -f "$PROGRESS_FILE" ]]; then - cp "$PROGRESS_FILE" "$archive_path/" - fi - - if [[ -f "$GUARDRAILS_FILE" ]]; then - cp "$GUARDRAILS_FILE" "$archive_path/" - fi - - if [[ -f "$ACTIVITY_LOG" ]]; then - cp "$ACTIVITY_LOG" "$archive_path/" - fi - - if [[ -f "$ERRORS_LOG" ]]; then - cp "$ERRORS_LOG" "$archive_path/" - fi - - echo "Archived previous run (branch: $branch_to_archive) to $archive_path" -} - -# Reset progress.txt to initial state -reset_progress_file() { - cat > "$PROGRESS_FILE" <&2 - mv "$PROGRESS_FILE" "${PROGRESS_FILE}.bak" - reset_progress_file - fi -} - -# Template for guardrails.md content -get_guardrails_template() { - cat <<'EOF' -# Ralph Guardrails - -Lessons learned and patterns discovered during Ralph loop iterations. - -## Lessons Learned -(No lessons recorded yet) - -## Patterns Discovered -(No patterns discovered yet) - -EOF -} - -# Ensure guardrails.md has proper structure -ensure_guardrails_file() { - if [[ ! -f "$GUARDRAILS_FILE" ]]; then - get_guardrails_template > "$GUARDRAILS_FILE" - elif ! grep -q "# Ralph Guardrails" "$GUARDRAILS_FILE"; then - echo "Warning: guardrails.md missing header, re-initializing" >&2 - mv "$GUARDRAILS_FILE" "${GUARDRAILS_FILE}.bak" - get_guardrails_template > "$GUARDRAILS_FILE" - fi -} - -# Show progress summary -show_progress_summary() { - [[ ! -f "$PRD_FILE" ]] && return - - local total completed pending - total=$(jq -r '.userStories | length' "$PRD_FILE") - completed=$(jq -r '[.userStories[] | select(.status == "done")] | length' "$PRD_FILE") - pending=$((total - completed)) - - echo "" - echo "=== Progress: $completed/$total stories completed, $pending pending ===" - echo "" -} - -# === Main initialization === - -# Read PRD data once -PRD_DATA=$(cat "$PRD_FILE") -BRANCH_NAME=$(echo "$PRD_DATA" | jq -r '.branchName') - -# Check if we need to archive previous run -if [[ -f "$LAST_BRANCH_FILE" ]]; then - LAST_BRANCH=$(cat "$LAST_BRANCH_FILE") - if [[ "$BRANCH_NAME" != "null" ]] && [[ -n "$BRANCH_NAME" ]] && [[ "$BRANCH_NAME" != "$LAST_BRANCH" ]]; then - archive_previous_run "$LAST_BRANCH" - reset_progress_file - fi -fi - -# Update .last-branch with current branch -echo "$BRANCH_NAME" > "$LAST_BRANCH_FILE" - -# Ensure we're on the correct branch -ensure_branch "$BRANCH_NAME" - -# Ensure state files exist -ensure_progress_file -ensure_guardrails_file - -# Ensure log files exist (create empty if not) -touch "$ACTIVITY_LOG" 2>/dev/null || true -touch "$ERRORS_LOG" 2>/dev/null || true - -# === Check for existing loop === -if [[ -f "$PID_FILE" ]]; then - OLD_PID=$(cat "$PID_FILE") - if kill -0 "$OLD_PID" 2>/dev/null; then - echo "Error: Ralph loop already running (PID $OLD_PID). Run /cancel-ralph first." >&2 - exit 1 - fi - rm -f "$PID_FILE" -fi - -# === Create log directory === -mkdir -p "$LOG_DIR" - -# === Setup cleanup handlers === -cleanup() { - local exit_code=$? - rm -f "$PID_FILE" - if [[ $exit_code -ne 0 ]]; then - echo "Ralph loop terminated with exit code $exit_code" >&2 - echo "Log directory: $LOG_DIR" >&2 - fi -} - -trap cleanup EXIT -trap 'echo "Received SIGINT, cleaning up..."; exit 130' INT -trap 'echo "Received SIGTERM, cleaning up..."; exit 143' TERM - -# === Record PID === -echo $$ > "$PID_FILE" - -# === Main loop === -echo "Ralph loop started: max $MAX_ITERATIONS iterations" -echo "" - -for i in $(seq 1 "$MAX_ITERATIONS"); do - echo "=== Ralph iteration $i/$MAX_ITERATIONS ===" - - # Build prompt from template - PROMPT=$(sed "s/{{ITERATION}}/$i/g; s/{{MAX}}/$MAX_ITERATIONS/g" "$PROMPT_TEMPLATE") - - # Log file for this iteration - ITERATION_LOG="$LOG_DIR/iteration-$i.log" - - # Track iteration start time - iteration_start=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - - # Run fresh Claude instance - echo "Starting iteration $i at $(date)" > "$ITERATION_LOG" - if ! OUTPUT=$(echo "$PROMPT" | claude --print --dangerously-skip-permissions 2>&1 | tee -a "$ITERATION_LOG"); then - exit_code=$? - iteration_end=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - echo "Warning: Claude command failed with exit code $exit_code" >&2 - echo "Check log: $ITERATION_LOG" >&2 - # Log error to errors.log - echo "${iteration_end} - Iteration $i failed with exit code ${exit_code}" >> "$ERRORS_LOG" - # Continue on transient errors (exit code 1), abort on fatal errors (> 1) - if [[ $exit_code -gt 1 ]]; then - # Log activity before exiting - echo "${iteration_end} - Iteration $i: START=${iteration_start}, END=${iteration_end}, STATUS=FATAL_ERROR" >> "$ACTIVITY_LOG" - exit $exit_code - fi - # Log failed iteration to activity - echo "${iteration_end} - Iteration $i: START=${iteration_start}, END=${iteration_end}, STATUS=ERROR" >> "$ACTIVITY_LOG" - else - # Track iteration end time on success - iteration_end=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - # Log successful iteration to activity - echo "${iteration_end} - Iteration $i: START=${iteration_start}, END=${iteration_end}, STATUS=SUCCESS" >> "$ACTIVITY_LOG" - fi - echo "Finished iteration $i at $(date)" >> "$ITERATION_LOG" - - # Check for completion promise - if echo "$OUTPUT" | grep -q 'COMPLETE'; then - echo "" - echo "=== Ralph completed at iteration $i ===" - show_progress_summary - exit 0 - fi - - echo "" - echo "--- Iteration $i finished, continuing... ---" - show_progress_summary - - # Sleep to prevent API rate limiting - sleep 2 -done - -echo "=== Ralph reached max iterations ($MAX_ITERATIONS) ===" -exit 1 diff --git a/skills/suggest-compacting/SKILL.md b/skills/suggest-compacting/SKILL.md deleted file mode 100644 index 450ae7a5..00000000 --- a/skills/suggest-compacting/SKILL.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -name: suggest-compacting -description: Suggests manual /compact at logical intervals during long sessions ---- - -# Suggest Compacting - -Suggests when to manually compact context during long Claude Code sessions. - -**Core principle**: The plugin suggests; you decide when to compact. - -## When to Use - -Suggest Compacting is automatically active during long sessions with significant file editing activity. - -**Automatic activation criteria:** -- After 50 Edit/Write tool calls (default threshold) -- Every 25 calls after threshold -- Configurable via `COMPACT_THRESHOLD` environment variable - -**Manual activation:** -- When you notice context becoming stale -- Before starting new implementation phase -- After completing major milestones - -## How It Works - -The Suggest Compacting hook tracks file editing activity: - -1. **Session-based tracking**: Counts Edit/Write tool calls per session -2. **Persistent state**: Counter persists via `~/.claude/suggest-compacting/tool-count-{session_id}.txt` -3. **Non-blocking suggestions**: Shows stderr messages that don't interrupt workflow -4. **Session isolation**: Each Claude Code session has its own counter - -### Messages You'll See - -At threshold (default: 50): -``` -[SuggestCompacting] 50 tool calls reached - consider /compact if transitioning phases -``` - -Every 25 calls after threshold: -``` -[SuggestCompacting] 75 tool calls - good checkpoint for /compact if context is stale -``` - -## Best Practices - -### When to Compact - -**Good timing:** -- After exploration phase, before implementation -- After completing a milestone (feature, bugfix, refactor) -- Before starting new unrelated task -- When context feels stale or repetitive -- After debugging, before writing fix - -**Bad timing:** -- Mid-implementation of current task -- Before understanding the problem -- When actively debugging -- During code review feedback - -### Compaction Strategy - -**What to keep:** -- Current task context and requirements -- Relevant architectural decisions -- Recent test results -- Current debugging findings - -**What to summarize:** -- Completed implementation details -- Historical conversation not relevant to current task -- Explored alternatives not chosen -- Past debugging attempts - -### Workflow Integration - -1. **Exploration phase**: Read code, understand problem -2. **Compact**: `/compact` - summarize findings, preserve context -3. **Implementation phase**: Write code, test -4. **Compact**: `/compact` - preserve implementation context -5. **Next phase**: Repeat as needed - -## Configuration - -### Custom Threshold - -Set `COMPACT_THRESHOLD` to customize when suggestions appear: - -```bash -# In your shell profile or session -export COMPACT_THRESHOLD=100 # Suggest after 100 tool calls -``` - -Default: 50 tool calls - -### State Directory - -Session counters stored in: -``` -~/.claude/suggest-compacting/tool-count-{session_id}.txt -``` - -Session ID extracted from SessionStart hook and stored in: -``` -~/.claude/suggest-compacting/session-env.sh -``` - -## Why Suggestions Over Forced Auto-Compaction? - -**Forced auto-compaction problems:** -- Happens at arbitrary points, often mid-task -- Loses critical context during active work -- Interrupts thought processes -- Difficult to resume after compaction - -**Suggest Compacting benefits:** -- You control when compaction occurs -- Preserve context through logical phases -- Choose natural breakpoints -- Maintain continuity of work - -## Use Cases - -### Long Debugging Sessions -1. Explore symptoms and reproduce bug -2. **Compact** (after finding root cause) -3. Implement fix -4. Test and verify - -### Multi-Phase Features -1. Research and design -2. **Compact** (after exploration) -3. Implementation phase 1 -4. **Compact** (after milestone) -5. Implementation phase 2 - -### Refactoring Work -1. Analyze current code -2. **Compact** (after understanding) -3. Refactor module A -4. **Compact** (after module A) -5. Refactor module B - -### Research-to-Code Transitions -1. Research problem domain -2. **Compact** (before writing code) -3. Implement solution - -## Common Mistakes - -| Mistake | Fix | -|---------|-----| -| Compacting mid-implementation | Wait for natural breakpoints | -| Compacting too frequently | Use 50-call threshold as guide | -| Not compacting at all | Context becomes stale, performance degrades | -| Compacting without summarizing | Preserves wrong context | - -## Quick Reference - -| Activity | Tool Calls | Action | -|----------|-----------|--------| -| Initial exploration | 0-50 | Continue normally | -| Threshold reached | 50 | Consider /compact if transitioning | -| Implementation | 50-100 | Check context at 75 | -| Milestone complete | 100+ | /compact before next phase | - -## Integration with Other Tools - -Suggest Compacting works well with: -- **Git workflows**: Compact before committing -- **TDD**: Compact after red-green-refactor cycle -- **Code review**: Compact after implementing feedback -- **Documentation**: Compact before writing docs diff --git a/skills/suggest-compacting/strategic-compact/SKILL.md b/skills/suggest-compacting/strategic-compact/SKILL.md deleted file mode 100644 index 0476e11c..00000000 --- a/skills/suggest-compacting/strategic-compact/SKILL.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -name: strategic-compact -description: Suggests manual /compact at logical intervals during long sessions ---- - -# Strategic Compact - -Context compaction suggestions for long Claude Code sessions. - -**Core principle**: Manual compaction at logical breakpoints > automatic compaction at arbitrary moments. - -## When to Use - -Strategic Compact is automatically active during long sessions with significant file editing activity. - -**Automatic activation criteria:** -- After 50 Edit/Write tool calls (default threshold) -- Every 25 calls after threshold -- Configurable via `COMPACT_THRESHOLD` environment variable - -**Manual activation:** -- When you notice context becoming stale -- Before starting new implementation phase -- After completing major milestones - -## How It Works - -The Strategic Compact hook tracks file editing activity: - -1. **Session-based tracking**: Counts Edit/Write tool calls per session -2. **Persistent state**: Counter persists via `~/.claude/strategic-compact/tool-count-{session_id}.txt` -3. **Non-blocking suggestions**: Shows stderr messages that don't interrupt workflow -4. **Session isolation**: Each Claude Code session has its own counter - -### Messages You'll See - -At threshold (default: 50): -``` -[StrategicCompact] 50 tool calls reached - consider /compact if transitioning phases -``` - -Every 25 calls after threshold: -``` -[StrategicCompact] 75 tool calls - good checkpoint for /compact if context is stale -``` - -## Best Practices - -### When to Compact - -**Good timing:** -- After exploration phase, before implementation -- After completing a milestone (feature, bugfix, refactor) -- Before starting new unrelated task -- When context feels stale or repetitive -- After debugging, before writing fix - -**Bad timing:** -- Mid-implementation of current task -- Before understanding the problem -- When actively debugging -- During code review feedback - -### Compaction Strategy - -**What to keep:** -- Current task context and requirements -- Relevant architectural decisions -- Recent test results -- Current debugging findings - -**What to summarize:** -- Completed implementation details -- Historical conversation not relevant to current task -- Explored alternatives not chosen -- Past debugging attempts - -### Workflow Integration - -1. **Exploration phase**: Read code, understand problem -2. **Compact**: `/compact` - summarize findings, preserve context -3. **Implementation phase**: Write code, test -4. **Compact**: `/compact` - preserve implementation context -5. **Next phase**: Repeat as needed - -## Configuration - -### Custom Threshold - -Set `COMPACT_THRESHOLD` to customize when suggestions appear: - -```bash -# In your shell profile or session -export COMPACT_THRESHOLD=100 # Suggest after 100 tool calls -``` - -Default: 50 tool calls - -### State Directory - -Session counters stored in: -``` -~/.claude/strategic-compact/tool-count-{session_id}.txt -``` - -Session ID extracted from SessionStart hook and stored in: -``` -~/.claude/strategic-compact/session-env.sh -``` - -## Why Manual Over Auto-Compact? - -**Automatic compaction problems:** -- Happens at arbitrary points, often mid-task -- Loses critical context during active work -- Interrupts thought processes -- Difficult to resume after compaction - -**Strategic compacting benefits:** -- Control over when compaction occurs -- Preserve context through logical phases -- Choose natural breakpoints -- Maintain continuity of work - -## Use Cases - -### Long Debugging Sessions -1. Explore symptoms and reproduce bug -2. **Compact** (after finding root cause) -3. Implement fix -4. Test and verify - -### Multi-Phase Features -1. Research and design -2. **Compact** (after exploration) -3. Implementation phase 1 -4. **Compact** (after milestone) -5. Implementation phase 2 - -### Refactoring Work -1. Analyze current code -2. **Compact** (after understanding) -3. Refactor module A -4. **Compact** (after module A) -5. Refactor module B - -### Research-to-Code Transitions -1. Research problem domain -2. **Compact** (before writing code) -3. Implement solution - -## Common Mistakes - -| Mistake | Fix | -|---------|-----| -| Compacting mid-implementation | Wait for natural breakpoints | -| Compacting too frequently | Use 50-call threshold as guide | -| Not compacting at all | Context becomes stale, performance degrades | -| Compacting without summarizing | Preserves wrong context | - -## Quick Reference - -| Activity | Tool Calls | Action | -|----------|-----------|--------| -| Initial exploration | 0-50 | Continue normally | -| Threshold reached | 50 | Consider /compact if transitioning | -| Implementation | 50-100 | Check context at 75 | -| Milestone complete | 100+ | /compact before next phase | - -## Integration with Other Tools - -Strategic Compact works well with: -- **Git workflows**: Compact before committing -- **TDD**: Compact after red-green-refactor cycle -- **Code review**: Compact after implementing feedback -- **Documentation**: Compact before writing docs diff --git a/src/suggest-compacting/auto-compact.ts b/src/suggest-compacting/auto-compact.ts deleted file mode 100755 index 0d66b78b..00000000 --- a/src/suggest-compacting/auto-compact.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { incrementState, isValidSessionId } from './lib/state.js'; - -interface PreToolUseInput { - tool_name: string; - session_id: string; -} - -const DEFAULT_THRESHOLD = 50; -const REPEAT_THRESHOLD = 25; - -async function main() { - const input = JSON.parse(await readStdin()) as PreToolUseInput; - - const sessionId = input.session_id || process.env.CLAUDE_SESSION_ID; - - if (!sessionId || !isValidSessionId(sessionId)) { - process.exit(0); - } - - const state = await incrementState(sessionId); - - const threshold = parseInt(process.env.COMPACT_THRESHOLD || `${DEFAULT_THRESHOLD}`, 10); - const shouldSuggest = state.count === threshold || - (state.count > threshold && (state.count - threshold) % REPEAT_THRESHOLD === 0); - - if (shouldSuggest) { - console.log(`\n--- Suggestion ---`); - console.log(`You've made ${state.count} tool calls in this session.`); - console.log(`Consider compacting your context to improve performance.`); - console.log(`Use: /compact`); - console.log(`------------------\n`); - } -} - -function readStdin(): Promise { - return new Promise((resolve) => { - let data = ''; - process.stdin.on('data', (chunk) => data += chunk); - process.stdin.on('end', () => resolve(data)); - }); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/src/suggest-compacting/lib/state.ts b/src/suggest-compacting/lib/state.ts deleted file mode 100644 index 0a6e1053..00000000 --- a/src/suggest-compacting/lib/state.ts +++ /dev/null @@ -1,45 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; - -interface ToolCountState { - count: number; - sessionId: string; -} - -const STATE_DIR = path.join(process.env.HOME || '', '.claude', 'suggest-compacting'); -const STATE_FILE = (sessionId: string) => path.join(STATE_DIR, `tool-count-${sessionId}.txt`); - -// Session ID validation (alphanumeric, underscore, hyphen only) -export function isValidSessionId(sessionId: string): boolean { - return /^[a-zA-Z0-9_-]+$/.test(sessionId); -} - -// Read state -export async function readState(sessionId: string): Promise { - const filepath = STATE_FILE(sessionId); - try { - const content = await fs.readFile(filepath, 'utf-8'); - const count = parseInt(content.trim(), 10); - return { count, sessionId }; - } catch { - return null; - } -} - -// Write state -export async function writeState(state: ToolCountState): Promise { - await fs.mkdir(STATE_DIR, { recursive: true }); - const filepath = STATE_FILE(state.sessionId); - await fs.writeFile(filepath, state.count.toString(), 'utf-8'); -} - -// Increment state -export async function incrementState(sessionId: string): Promise { - const current = await readState(sessionId); - const newState: ToolCountState = { - count: (current?.count ?? 0) + 1, - sessionId, - }; - await writeState(newState); - return newState; -} diff --git a/src/suggest-compacting/package.json b/src/suggest-compacting/package.json deleted file mode 100644 index d66d6a42..00000000 --- a/src/suggest-compacting/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "suggest-compacting-plugin", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "build": "tsc", - "clean": "rm -rf dist", - "prepare": "bun run build" - }, - "devDependencies": { - "typescript": "^5.3.3" - } -} diff --git a/src/suggest-compacting/session-start.ts b/src/suggest-compacting/session-start.ts deleted file mode 100755 index 0b51672b..00000000 --- a/src/suggest-compacting/session-start.ts +++ /dev/null @@ -1,34 +0,0 @@ -import fs from 'fs/promises'; -import { isValidSessionId } from './lib/state.js'; - -interface SessionStartInput { - session_id: string; - transcript_path: string; -} - -async function main() { - const input = JSON.parse(await readStdin()) as SessionStartInput; - - if (!isValidSessionId(input.session_id)) { - console.error(`Invalid session_id: ${input.session_id}`); - process.exit(1); - } - - const envFile = process.env.CLAUDE_ENV_FILE; - if (envFile) { - await fs.appendFile(envFile, `CLAUDE_SESSION_ID=${input.session_id}\n`); - } -} - -function readStdin(): Promise { - return new Promise((resolve) => { - let data = ''; - process.stdin.on('data', (chunk) => data += chunk); - process.stdin.on('end', () => resolve(data)); - }); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/src/suggest-compacting/tsconfig.json b/src/suggest-compacting/tsconfig.json deleted file mode 100644 index 2c11cfc0..00000000 --- a/src/suggest-compacting/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"], - "exclude": ["dist"] -} diff --git a/tests/helpers/bats_helper.bash b/tests/helpers/bats_helper.bash index 6d3de034..4049902b 100644 --- a/tests/helpers/bats_helper.bash +++ b/tests/helpers/bats_helper.bash @@ -178,11 +178,28 @@ validate_plugin_manifest_fields() { } # Helper: Iterate over all plugin manifest files +# Includes both root canonical plugin and plugins directory plugins. # Usage: for_each_plugin_manifest callback_function for_each_plugin_manifest() { local callback="$1" - local manifest_files - manifest_files=$(find "$PROJECT_ROOT/plugins" -name "plugin.json" -type f 2>/dev/null) + local manifest_files="" + + # Check for root canonical plugin + local root_manifest="$PROJECT_ROOT/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + manifest_files="$root_manifest"$'\n' + fi + + # Find all plugin manifests in plugins directory + local plugins_manifests + plugins_manifests=$(find "$PROJECT_ROOT/plugins" -name "plugin.json" -type f 2>/dev/null) + + if [ -n "$plugins_manifests" ]; then + manifest_files="${manifest_files}${plugins_manifests}" + fi + + # Trim trailing newline if manifest_files is not empty + manifest_files=$(echo "$manifest_files" | grep -v '^$') [ -n "$manifest_files" ] || return 1 diff --git a/tests/helpers/bun.ts b/tests/helpers/bun.ts index 4638f92d..c6ef0878 100644 --- a/tests/helpers/bun.ts +++ b/tests/helpers/bun.ts @@ -4,7 +4,7 @@ */ import { readFileSync, existsSync, readdirSync } from 'fs' -import { join, dirname, relative } from 'path' +import { join, dirname } from 'path' import { fileURLToPath } from 'url' // Get the current file path and resolve project root @@ -183,10 +183,17 @@ export function validatePluginManifestFields(manifest: PluginManifest, path: str /** * Get all plugin manifest paths + * Includes both root canonical plugin and plugins directory plugins. */ export function getAllPluginManifests(): string[] { const manifests: string[] = [] + // Check for root canonical plugin + const rootManifestPath = join(PROJECT_ROOT, '.claude-plugin', 'plugin.json') + if (existsSync(rootManifestPath)) { + manifests.push(rootManifestPath) + } + try { const plugins = readdirSync(PLUGINS_DIR, { withFileTypes: true }) @@ -200,7 +207,6 @@ export function getAllPluginManifests(): string[] { } } catch (error) { // Plugins directory might not exist - return [] } return manifests @@ -388,25 +394,81 @@ export function getAllPluginDirectories(): string[] { /** * Check if all plugins are listed in marketplace.json + * Supports both root canonical plugin (source: "./") and plugins directory plugins (source: "./plugins/") */ export function validateMarketplaceIncludesAllPlugins( marketplace: MarketplaceManifest, marketplacePath: string ): void { const pluginDirs = getAllPluginDirectories() - const marketplacePlugins = marketplace.plugins - .map((p) => { - // Extract plugin name from source path like "./plugins/git-guard" - const match = p.source.match(/^\.\/plugins\/([^/]+)$/) - return match ? match[1] : null - }) - .filter((name): name is string => name !== null) + + // Extract plugin names from marketplace - support both "./" and "./plugins/" formats + const marketplacePluginNames = new Set() + + for (const p of marketplace.plugins) { + if (p.name) { + // Use explicit name if available + marketplacePluginNames.add(p.name) + } else { + // Fallback: extract from source path + // "./" means root plugin, "./plugins/" means plugins directory + if (p.source === './') { + // Root plugin - need to read its name from plugin.json + const rootManifestPath = join(PROJECT_ROOT, '.claude-plugin', 'plugin.json') + if (existsSync(rootManifestPath)) { + try { + const manifest = validateJson(rootManifestPath) + if (manifest.name) { + marketplacePluginNames.add(manifest.name) + } + } catch { + // Ignore parse errors + } + } + } else { + // Extract from "./plugins/" format + const match = p.source.match(/^\.\/plugins\/([^/]+)$/) + if (match) { + marketplacePluginNames.add(match[1]) + } + } + } + } + + // Check for root canonical plugin + const rootPluginJsonPath = join(PROJECT_ROOT, '.claude-plugin', 'plugin.json') + if (existsSync(rootPluginJsonPath)) { + try { + const rootManifest = validateJson(rootPluginJsonPath) + if (rootManifest.name && !marketplacePluginNames.has(rootManifest.name)) { + throw new Error( + `Root plugin '${rootManifest.name}' missing from ${marketplacePath}` + ) + } + } catch (error) { + // Re-throw if it's our validation error + if (error instanceof Error && error.message.includes('missing from')) { + throw error + } + // Ignore other parse errors + } + } const missingPlugins: string[] = [] for (const pluginDir of pluginDirs) { - if (!marketplacePlugins.includes(pluginDir)) { - missingPlugins.push(pluginDir) + // Get the actual plugin name from its manifest + const manifestPath = join(PLUGINS_DIR, pluginDir, '.claude-plugin', 'plugin.json') + try { + const manifest = validateJson(manifestPath) + if (manifest.name && !marketplacePluginNames.has(manifest.name)) { + missingPlugins.push(manifest.name) + } + } catch { + // Fallback to directory name if manifest is invalid + if (!marketplacePluginNames.has(pluginDir)) { + missingPlugins.push(pluginDir) + } } } diff --git a/tests/helpers/marketplace_helper.bash b/tests/helpers/marketplace_helper.bash index 05ec3557..6445184b 100644 --- a/tests/helpers/marketplace_helper.bash +++ b/tests/helpers/marketplace_helper.bash @@ -69,11 +69,9 @@ marketplace_plugin_exists() { return 1 fi - # Check if plugin name exists in plugins array + # Check if plugin name exists in plugins array using .name field $JQ_BIN -e --arg name "$plugin_name" \ - '.plugins[].source' "$marketplace_file" 2>/dev/null | \ - sed 's|^\./plugins/||' | \ - grep -q "^${plugin_name}$" + '.plugins[] | select(.name == $name)' "$marketplace_file" > /dev/null 2>&1 } # Check if all plugins listed in marketplace.json exist in the filesystem @@ -117,6 +115,7 @@ marketplace_all_plugins_exist() { } # Check if all plugins in plugins/ directory are listed in marketplace.json +# Also checks for root canonical plugin if it exists. # Args: # $1 - (Optional) Path to marketplace.json (defaults to MARKETPLACE_JSON) # Returns: @@ -135,9 +134,23 @@ marketplace_all_plugins_listed() { return 1 fi - # Get plugins from marketplace (extract just plugin name from source path) - local marketplace_plugins - marketplace_plugins=$($JQ_BIN -r '.plugins[].source' "$marketplace_file" 2>/dev/null | sed 's|^\./plugins/||') + # Get plugin names from marketplace using .name field + local marketplace_plugin_names + marketplace_plugin_names=$($JQ_BIN -r '.plugins[].name' "$marketplace_file" 2>/dev/null) + + # Check for root canonical plugin + local root_plugin_json="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_plugin_json" ]; then + local root_plugin_name + root_plugin_name=$($JQ_BIN -r '.name' "$root_plugin_json" 2>/dev/null) + + if [ -n "$root_plugin_name" ] && [ "$root_plugin_name" != "null" ]; then + if ! echo "$marketplace_plugin_names" | grep -q "^${root_plugin_name}$"; then + echo "Error: Root plugin '$root_plugin_name' not listed in marketplace.json" >&2 + ((missing++)) + fi + fi + fi # Check each plugin directory plugin_dirs=$(find "${PROJECT_ROOT}/plugins" -mindepth 1 -maxdepth 1 -type d ! -name ".*" 2>/dev/null | sort) @@ -153,8 +166,8 @@ marketplace_all_plugins_listed() { continue fi - # Check if plugin is in marketplace.json - if ! echo "$marketplace_plugins" | grep -q "^${plugin_name}$"; then + # Check if plugin is in marketplace.json by name + if ! echo "$marketplace_plugin_names" | grep -q "^${plugin_name}$"; then echo "Error: Plugin '$plugin_name' not listed in marketplace.json" >&2 ((missing++)) fi diff --git a/tests/helpers/test_utils.bash b/tests/helpers/test_utils.bash index 34b634a9..5329bcb4 100755 --- a/tests/helpers/test_utils.bash +++ b/tests/helpers/test_utils.bash @@ -527,11 +527,14 @@ check_marketplace_plugins_exist() { sources=$(get_marketplace_plugins "$marketplace_file") while IFS= read -r source; do + [ -z "$source" ] && continue + local full_path="${PROJECT_ROOT}/${source}" if [ ! -d "$full_path" ]; then echo "Error: Plugin source '$source' does not exist" >&2 ((missing++)) + continue fi if [ ! -f "${full_path}/.claude-plugin/plugin.json" ]; then @@ -544,6 +547,7 @@ check_marketplace_plugins_exist() { } # Check if all plugins in plugins/ are listed in marketplace.json +# Also checks for root canonical plugin if it exists. # Returns: # 0 if all listed, 1 otherwise (outputs missing plugins to stderr) # Usage: @@ -560,14 +564,30 @@ check_all_plugins_in_marketplace() { return 1 fi - # Get plugins from marketplace (extract just plugin name from source path) - local marketplace_plugins - marketplace_plugins=$($JQ_BIN -r '.plugins[].source' "$marketplace_file" 2>/dev/null | sed 's|^\./plugins/||') + # Get plugin names from marketplace using .name field + local marketplace_plugin_names + marketplace_plugin_names=$($JQ_BIN -r '.plugins[].name' "$marketplace_file" 2>/dev/null) + + # Check for root canonical plugin + local root_plugin_json="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_plugin_json" ]; then + local root_plugin_name + root_plugin_name=$($JQ_BIN -r '.name' "$root_plugin_json" 2>/dev/null) + + if [ -n "$root_plugin_name" ] && [ "$root_plugin_name" != "null" ]; then + if ! echo "$marketplace_plugin_names" | grep -q "^${root_plugin_name}$"; then + echo "Error: Root plugin '$root_plugin_name' not listed in marketplace.json" >&2 + ((missing++)) + fi + fi + fi # Check each plugin directory plugin_dirs=$(find_all_plugins) while IFS= read -r plugin_dir; do + [ -z "$plugin_dir" ] && continue + local plugin_name plugin_name=$(basename "$plugin_dir") @@ -576,8 +596,8 @@ check_all_plugins_in_marketplace() { continue fi - # Check if plugin is in marketplace.json - if ! echo "$marketplace_plugins" | grep -q "^${plugin_name}$"; then + # Check if plugin is in marketplace.json by name + if ! echo "$marketplace_plugin_names" | grep -q "^${plugin_name}$"; then echo "Error: Plugin '$plugin_name' not listed in marketplace.json" >&2 ((missing++)) fi diff --git a/tests/marketplace_json.bats b/tests/marketplace_json.bats index 683ca0a5..1a8fdf11 100644 --- a/tests/marketplace_json.bats +++ b/tests/marketplace_json.bats @@ -43,3 +43,18 @@ setup() { @test "marketplace.json plugin sources point to existing directories" { marketplace_all_plugins_exist "$MARKETPLACE_JSON" } + +@test "marketplace.json includes root everything-agent plugin" { + # Check if root plugin exists + local root_plugin_json="${PROJECT_ROOT}/.claude-plugin/plugin.json" + + if [ ! -f "$root_plugin_json" ]; then + skip "Root plugin manifest not found" + fi + + local root_plugin_name + root_plugin_name=$(json_get "$root_plugin_json" "name") + + # Verify root plugin is listed in marketplace.json + marketplace_plugin_exists "$root_plugin_name" "$MARKETPLACE_JSON" +} diff --git a/tests/me/me-specific.bats b/tests/me/me-specific.bats deleted file mode 100644 index 50607bf8..00000000 --- a/tests/me/me-specific.bats +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bats -# Consolidated plugin structure tests -# Tests for components that were previously in the me plugin - -load ../helpers/bats_helper - -@test "me: has all workflow commands" { - [ -f "${PROJECT_ROOT}/commands/brainstorm.md" ] - [ -f "${PROJECT_ROOT}/commands/debugging.md" ] - [ -f "${PROJECT_ROOT}/commands/orchestrate.md" ] - [ -f "${PROJECT_ROOT}/commands/refactor-clean.md" ] - [ -f "${PROJECT_ROOT}/commands/research.md" ] - [ -f "${PROJECT_ROOT}/commands/verify.md" ] -} - -@test "me: code-reviewer agent exists with proper model" { - local agent_file="${PROJECT_ROOT}/agents/code-reviewer.md" - [ -f "$agent_file" ] - has_frontmatter_field "$agent_file" "model" -} - -# create-pr skill tests -@test "me: create-pr skill exists with required components" { - [ -f "${PROJECT_ROOT}/skills/create-pr/SKILL.md" ] - [ -f "${PROJECT_ROOT}/skills/create-pr/scripts/check-conflicts.sh" ] - [ -f "${PROJECT_ROOT}/skills/create-pr/scripts/verify-pr-status.sh" ] - [ -f "${PROJECT_ROOT}/skills/create-pr/scripts/sync-with-base.sh" ] -} - -@test "me: create-pr skill has proper frontmatter" { - local skill_file="${PROJECT_ROOT}/skills/create-pr/SKILL.md" - has_frontmatter_delimiter "$skill_file" - has_frontmatter_field "$skill_file" "name" - has_frontmatter_field "$skill_file" "description" -} - -@test "me: create-pr scripts are executable" { - [ -x "${PROJECT_ROOT}/skills/create-pr/scripts/check-conflicts.sh" ] - [ -x "${PROJECT_ROOT}/skills/create-pr/scripts/verify-pr-status.sh" ] - [ -x "${PROJECT_ROOT}/skills/create-pr/scripts/sync-with-base.sh" ] -} - -@test "me: create-pr check-conflicts.sh validates git repo" { - local script="${PROJECT_ROOT}/skills/create-pr/scripts/check-conflicts.sh" - grep -q "git rev-parse.*git-dir" "$script" -} - -@test "me: create-pr verify-pr-status.sh handles all PR states with CI checks" { - local script="${PROJECT_ROOT}/skills/create-pr/scripts/verify-pr-status.sh" - grep -q "CLEAN)" "$script" - grep -q "BEHIND)" "$script" - grep -q "DIRTY)" "$script" - grep -q "statusCheckRollup" "$script" - grep -q "isRequired" "$script" - grep -q "BLOCKED|UNSTABLE" "$script" -} diff --git a/tests/performance/benchmarks.bats b/tests/performance/benchmarks.bats index 4d562911..e3ad78ad 100644 --- a/tests/performance/benchmarks.bats +++ b/tests/performance/benchmarks.bats @@ -43,6 +43,18 @@ benchmark_end() { local total_time=0 local plugin_count=0 + # Check root canonical plugin + local root_plugin_dir="${PROJECT_ROOT}" + if [ -f "${root_plugin_dir}/.claude-plugin/plugin.json" ]; then + benchmark_start + parse_plugin_json "$root_plugin_dir" > /dev/null + local elapsed + elapsed=$(benchmark_end) + total_time=$((total_time + elapsed)) + plugin_count=$((plugin_count + 1)) + fi + + # Check plugins directory for plugin_dir in "${PROJECT_ROOT}"/plugins/*/; do if [ -d "$plugin_dir" ]; then benchmark_start @@ -54,6 +66,8 @@ benchmark_end() { fi done + [ "$plugin_count" -gt 0 ] || skip "No plugins found" + local avg_time=$((total_time / plugin_count)) echo "Parsed $plugin_count plugins in ${total_time}ms (avg: ${avg_time}ms each)" @@ -77,6 +91,18 @@ benchmark_end() { local total_time=0 local file_count=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + benchmark_start + validate_json "$root_manifest" > /dev/null + local elapsed + elapsed=$(benchmark_end) + total_time=$((total_time + elapsed)) + file_count=$((file_count + 1)) + fi + + # Check plugins directory for json_file in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$json_file" ]; then benchmark_start @@ -88,6 +114,8 @@ benchmark_end() { fi done + [ "$file_count" -gt 0 ] || skip "No plugin.json files found" + local avg_time=$((total_time / file_count)) echo "Validated $file_count JSON files in ${total_time}ms (avg: ${avg_time}ms each)" @@ -173,6 +201,13 @@ benchmark_end() { # Run a representative set of operations get_all_plugins > /dev/null + # Check root canonical plugin + local root_plugin_dir="${PROJECT_ROOT}" + if [ -f "${root_plugin_dir}/.claude-plugin/plugin.json" ]; then + parse_plugin_json "$root_plugin_dir" > /dev/null + fi + + # Check plugins directory for plugin_dir in "${PROJECT_ROOT}"/plugins/*/; do if [ -d "$plugin_dir" ]; then parse_plugin_json "$plugin_dir" > /dev/null diff --git a/tests/plugin_json.bats b/tests/plugin_json.bats index ece5c259..25ab9047 100644 --- a/tests/plugin_json.bats +++ b/tests/plugin_json.bats @@ -13,6 +13,13 @@ setup() { @test "plugin.json exists" { local found=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + found=$((found + 1)) + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then found=$((found + 1)) @@ -26,6 +33,15 @@ setup() { @test "plugin.json is valid JSON" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + if ! validate_json "$root_manifest"; then + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then if ! validate_json "$manifest"; then @@ -42,6 +58,18 @@ setup() { local failed=0 local required_fields=("name" "description" "author") + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + for field in "${required_fields[@]}"; do + if ! json_has_field "$root_manifest" "$field"; then + echo "Missing '$field' in $root_manifest" >&2 + ((failed++)) + fi + done + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then for field in "${required_fields[@]}"; do @@ -60,6 +88,18 @@ setup() { @test "plugin.json name follows naming convention" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + local name + name=$(json_get "$root_manifest" "name") + if ! is_valid_plugin_name "$name"; then + echo "Invalid plugin name '$name' in $root_manifest" >&2 + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then local name @@ -79,6 +119,20 @@ setup() { local failed=0 local fields_to_check=("name" "description" "author") + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + for field in "${fields_to_check[@]}"; do + local value + value=$(json_get "$root_manifest" "$field") + if [ -z "$value" ]; then + echo "Field '$field' is empty in $root_manifest" >&2 + ((failed++)) + fi + done + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then for field in "${fields_to_check[@]}"; do @@ -99,6 +153,15 @@ setup() { @test "plugin.json uses only allowed fields" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + if ! validate_plugin_manifest_fields "$root_manifest"; then + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then if ! validate_plugin_manifest_fields "$manifest"; then diff --git a/tests/plugin_validation_common.bats b/tests/plugin_validation_common.bats index 9787cb2a..1fab125e 100644 --- a/tests/plugin_validation_common.bats +++ b/tests/plugin_validation_common.bats @@ -73,21 +73,42 @@ _assert_valid_plugin_name() { ############################################################################### @test "common: plugin.json files exist in plugin directories" { - local manifest_files - manifest_files=$(find "$PROJECT_ROOT/plugins" -name "plugin.json" -type f 2>/dev/null) + local manifest_files="" + local count=0 + + # Check root canonical plugin + local root_manifest="$PROJECT_ROOT/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + manifest_files="$root_manifest"$'\n' + count=$((count + 1)) + fi - [ -n "$manifest_files" ] || skip "No plugin.json files found" + # Find all plugin manifests in plugins directory + local plugins_manifests + plugins_manifests=$(find "$PROJECT_ROOT/plugins" -name "plugin.json" -type f 2>/dev/null) - # Count manifests found - use file count instead of loop - local count - count=$(echo "$manifest_files" | grep -c '^') + if [ -n "$plugins_manifests" ]; then + manifest_files="${manifest_files}${plugins_manifests}" + local plugins_count + plugins_count=$(echo "$plugins_manifests" | grep -c '^') + count=$((count + plugins_count)) + fi - [ "$count" -gt 0 ] + [ "$count" -gt 0 ] || skip "No plugin.json files found" } @test "common: all plugin.json files are valid JSON" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + if ! validate_json "$root_manifest"; then + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then if ! validate_json "$manifest"; then @@ -102,6 +123,15 @@ _assert_valid_plugin_name() { @test "common: all plugin.json files have required fields" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + if ! _assert_has_required_fields "$root_manifest"; then + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then if ! _assert_has_required_fields "$manifest"; then @@ -116,6 +146,15 @@ _assert_valid_plugin_name() { @test "common: all plugin.json files have non-empty required field values" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + if ! _assert_field_values_not_empty "$root_manifest"; then + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then if ! _assert_field_values_not_empty "$manifest"; then @@ -130,6 +169,15 @@ _assert_valid_plugin_name() { @test "common: all plugin.json names follow naming convention" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + if ! _assert_valid_plugin_name "$root_manifest"; then + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then if ! _assert_valid_plugin_name "$manifest"; then @@ -144,6 +192,15 @@ _assert_valid_plugin_name() { @test "common: all plugin.json files use only allowed fields" { local failed=0 + # Check root canonical plugin + local root_manifest="${PROJECT_ROOT}/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + if ! validate_plugin_manifest_fields "$root_manifest"; then + ((failed++)) + fi + fi + + # Check plugins directory for manifest in "${PROJECT_ROOT}"/plugins/*/.claude-plugin/plugin.json; do if [ -f "$manifest" ]; then if ! validate_plugin_manifest_fields "$manifest"; then diff --git a/tests/ralph_loop_command_tests.bats b/tests/ralph_loop_command_tests.bats deleted file mode 100755 index e13fc89b..00000000 --- a/tests/ralph_loop_command_tests.bats +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env bats -# Tests for ralph-loop plugin commands - -load helpers/bats_helper - -setup() { - # Get command file paths - CANCEL_RALPH_CMD="${PROJECT_ROOT}/commands/cancel-ralph.md" - RALPH_INIT_CMD="${PROJECT_ROOT}/commands/ralph-init.md" - RALPH_LOOP_CMD="${PROJECT_ROOT}/commands/ralph-loop.md" - - # Verify command files exist - if [[ ! -f "$CANCEL_RALPH_CMD" ]]; then - skip "cancel-ralph.md not found" - fi -} - -# cancel-ralph command tests -@test "cancel-ralph.md: exists" { - [ -f "$CANCEL_RALPH_CMD" ] -} - -@test "cancel-ralph.md: has valid frontmatter delimiter" { - has_frontmatter_delimiter "$CANCEL_RALPH_CMD" -} - -@test "cancel-ralph.md: has description field" { - has_frontmatter_field "$CANCEL_RALPH_CMD" "description" -} - -@test "cancel-ralph.md: description mentions canceling Ralph loop" { - grep "^description:" "$CANCEL_RALPH_CMD" | grep -q "Cancel" - grep "^description:" "$CANCEL_RALPH_CMD" | grep -q "Ralph" -} - -@test "cancel-ralph.md: has allowed-tools field" { - has_frontmatter_field "$CANCEL_RALPH_CMD" "allowed-tools" -} - -@test "cancel-ralph.md: allowed-tools includes kill command" { - grep "^allowed-tools:" "$CANCEL_RALPH_CMD" | grep -q "kill" -} - -@test "cancel-ralph.md: allowed-tools includes reading PID file" { - grep "^allowed-tools:" "$CANCEL_RALPH_CMD" | grep -q "cat .ralph/ralph.pid" -} - -@test "cancel-ralph.md: allowed-tools includes removing PID file" { - grep "^allowed-tools:" "$CANCEL_RALPH_CMD" | grep -q "rm .ralph/ralph.pid" -} - -@test "cancel-ralph.md: instructions mention .ralph/ralph.pid" { - grep -q ".ralph/ralph.pid" "$CANCEL_RALPH_CMD" -} - -@test "cancel-ralph.md: instructions mention checking if PID file exists" { - grep -q "exists" "$CANCEL_RALPH_CMD" -} - -@test "cancel-ralph.md: instructions mention killing the process" { - grep -q "kill" "$CANCEL_RALPH_CMD" -} - -@test "cancel-ralph.md: instructions mention removing PID file" { - grep -q "Remove" "$CANCEL_RALPH_CMD" && grep -q "PID" "$CANCEL_RALPH_CMD" -} - -@test "cancel-ralph.md: instructions mention reporting cancellation" { - grep -q "Report" "$CANCEL_RALPH_CMD" && grep -q "cancellation" "$CANCEL_RALPH_CMD" -} - -@test "cancel-ralph.md: does NOT mention .claude/ralph-loop.local.md" { - ! grep -q ".claude/ralph-loop.local.md" "$CANCEL_RALPH_CMD" -} - -@test "cancel-ralph.md: does NOT mention iteration field" { - ! grep -q "iteration:" "$CANCEL_RALPH_CMD" -} - -# ralph-init command tests -@test "ralph-init.md: exists" { - [ -f "$RALPH_INIT_CMD" ] -} - -@test "ralph-init.md: has valid frontmatter delimiter" { - has_frontmatter_delimiter "$RALPH_INIT_CMD" -} - -@test "ralph-init.md: has description field" { - has_frontmatter_field "$RALPH_INIT_CMD" "description" -} - -# ralph-loop command tests -@test "ralph-loop.md: exists" { - [ -f "$RALPH_LOOP_CMD" ] -} - -@test "ralph-loop.md: has valid frontmatter delimiter" { - has_frontmatter_delimiter "$RALPH_LOOP_CMD" -} - -@test "ralph-loop.md: has description field" { - has_frontmatter_field "$RALPH_LOOP_CMD" "description" -} - -@test "ralph-loop.md: has allowed-tools field" { - has_frontmatter_field "$RALPH_LOOP_CMD" "allowed-tools" -} - -@test "ralph-loop.md: mentions cancel-ralph in documentation" { - grep -q "cancel-ralph" "$RALPH_LOOP_CMD" -} - -# help.md command tests -HELP_CMD="${PROJECT_ROOT}/commands/ralph-help.md" - -@test "help.md: exists" { - [ -f "$HELP_CMD" ] -} - -@test "help.md: has valid frontmatter delimiter" { - has_frontmatter_delimiter "$HELP_CMD" -} - -@test "help.md: has description field" { - has_frontmatter_field "$HELP_CMD" "description" -} - -@test "help.md: mentions /ralph-init command" { - grep -q "/ralph-init" "$HELP_CMD" -} - -@test "help.md: mentions /ralph-loop command" { - grep -q "/ralph-loop" "$HELP_CMD" -} - -@test "help.md: mentions /cancel-ralph command" { - grep -q "/cancel-ralph" "$HELP_CMD" -} - -@test "help.md: explains PRD creation workflow" { - grep -q "PRD" "$HELP_CMD" || grep -q "Product Requirements Document" "$HELP_CMD" -} - -@test "help.md: explains fresh instance approach" { - grep -q "fresh" "$HELP_CMD" -} - -@test "help.md: explains bash loop mechanism" { - grep -q "bash loop" "$HELP_CMD" || grep -q "while" "$HELP_CMD" -} - -@test "help.md: mentions PRD file location" { - grep -q "\.ralph/prd\.json" "$HELP_CMD" -} - -@test "help.md: mentions progress tracking" { - grep -q "progress" "$HELP_CMD" -} - -@test "help.md: does NOT mention Stop hook" { - ! grep -q "Stop hook" "$HELP_CMD" -} - -@test "help.md: does NOT mention completion-promise" { - ! grep -q "completion-promise" "$HELP_CMD" -} - -@test "help.md: does NOT mention tags" { - ! grep -q "" "$HELP_CMD" -} - -@test "help.md: does NOT mention session-based looping" { - ! grep -q "session" "$HELP_CMD" || ! grep -q "current session" "$HELP_CMD" -} - -@test "help.md: credits snarktank/ralph inspiration" { - grep -q "snarktank" "$HELP_CMD" || grep -q "ghuntley" "$HELP_CMD" -} diff --git a/tests/ralph_loop_script_tests.bats b/tests/ralph_loop_script_tests.bats deleted file mode 100644 index 288313d7..00000000 --- a/tests/ralph_loop_script_tests.bats +++ /dev/null @@ -1,1538 +0,0 @@ -#!/usr/bin/env bats -# Tests for ralph.sh bash script - -load helpers/bats_helper - -setup() { - # Get script paths - RALPH_SCRIPT="${PROJECT_ROOT}/scripts/ralph/ralph.sh" - PROMPT_TEMPLATE="${PROJECT_ROOT}/scripts/ralph/prompt.md" - - # Create temp directory for test files - TEST_TEMP_DIR=$(mktemp -d -t ralph-test.XXXXXX) - export TEST_TEMP_DIR - export TEST_RALPH_DIR="${TEST_TEMP_DIR}/.ralph" - mkdir -p "$TEST_RALPH_DIR" - - # Create a fake git repo for testing - export TEST_GIT_DIR="${TEST_TEMP_DIR}/git-repo" - mkdir -p "$TEST_GIT_DIR" - cd "$TEST_GIT_DIR" - git init -q - git config user.email "test@example.com" - git config user.name "Test User" - - # Create mock claude command - export MOCK_CLAUDE="${TEST_TEMP_DIR}/claude" - cat > "$MOCK_CLAUDE" <<'EOF' -#!/bin/bash -# Mock claude command for testing -if [[ "$*" == *"--print"* ]]; then - # Read from stdin - cat - exit 0 -fi -echo "Mock claude command" -EOF - chmod +x "$MOCK_CLAUDE" - export PATH="${TEST_TEMP_DIR}:$PATH" -} - -teardown() { - # Kill any stray ralph.sh processes from this test - if [ -n "${TEST_TEMP_DIR:-}" ] && [ -f "${TEST_TEMP_DIR}/.ralph/ralph.pid" ]; then - kill "$(cat "${TEST_TEMP_DIR}/.ralph/ralph.pid")" 2>/dev/null || true - fi - - # Clean up temp directory - if [ -n "${TEST_TEMP_DIR:-}" ] && [ -d "$TEST_TEMP_DIR" ]; then - rm -rf "$TEST_TEMP_DIR" - fi - - # Return to original directory - cd "$PROJECT_ROOT" || true -} - -# Helper: Create a minimal PRD file -create_prd() { - local branch="${1:-ralph/test-feature}" - cat > "$TEST_RALPH_DIR/prd.json" < "$TEST_RALPH_DIR/progress.txt" </dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - - # Check if PID file was created - if [ -f .ralph/ralph.pid ]; then - pid=$(cat .ralph/ralph.pid) - # PID should be a number - [[ "$pid" =~ ^[0-9]+$ ]] - fi - - # Clean up - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true -} - -# Test: Script cleanup PID file on exit -@test "ralph.sh: cleans up PID file on exit" { - create_prd - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Run ralph.sh in background then kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # After script exits, PID file should be cleaned up by the trap - # Give it a moment to clean up - sleep 0.2 - - # PID file should not exist (cleaned up by EXIT trap) - if [ -f .ralph/ralph.pid ]; then - pid=$(cat .ralph/ralph.pid) - # If file exists, process should NOT be running - ! kill -0 "$pid" 2>/dev/null - fi -} - -# Test: Script detects existing running loop -@test "ralph.sh: detects existing running loop" { - create_prd - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create a fake PID file with current process PID - echo $$ > .ralph/ralph.pid - - # Try to run again - should fail - run bash "$RALPH_SCRIPT" 2 - [ $status -eq 1 ] - [[ "$output" == *"already running"* ]] -} - -# Test: Script checks out branch from prd.json -@test "ralph.sh: checks out branch from prd.json" { - local test_branch="ralph/test-feature-branch" - create_prd "$test_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Check if we're on the correct branch - current_branch=$(git branch --show-current) - [ "$current_branch" = "$test_branch" ] -} - -# Test: Script creates branch if it doesn't exist -@test "ralph.sh: creates branch if it doesn't exist" { - local test_branch="ralph/new-branch" - create_prd "$test_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit on main - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" - - # Ensure branch doesn't exist - git checkout -q main 2>/dev/null || git checkout -q master 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Check if we're on the new branch - current_branch=$(git branch --show-current) - [ "$current_branch" = "$test_branch" ] -} - -# Test: Script creates progress.txt if missing -@test "ralph.sh: creates progress.txt if missing" { - create_prd - # Don't create progress.txt - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # progress.txt should be created - [ -f .ralph/progress.txt ] - grep -q "Ralph Progress Log" .ralph/progress.txt -} - -# Test: prompt.md template has ITERATION placeholder -@test "prompt.md: has {{ITERATION}} placeholder" { - run grep -q "{{ITERATION}}" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template has MAX placeholder -@test "prompt.md: has {{MAX}} placeholder" { - run grep -q "{{MAX}}" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions PRD reading -@test "prompt.md: mentions reading .ralph/prd.json" { - run grep -q "\.ralph/prd\.json" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions progress.txt -@test "prompt.md: mentions reading .ralph/progress.txt" { - run grep -q "\.ralph/progress\.txt" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions COMPLETE promise -@test "prompt.md: mentions COMPLETE" { - run grep -q "COMPLETE" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions implementing one story -@test "prompt.md: mentions implementing ONE story" { - run grep -q "ONE story" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions updating prd.json -@test "prompt.md: mentions updating .ralph/prd.json" { - run grep -q "prd\.json" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions appending to progress.txt -@test "prompt.md: mentions appending to progress.txt" { - run grep -q "progress\.txt" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template has Progress Report Format section -@test "prompt.md: has Progress Report Format section" { - run grep -q "Progress Report Format" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template has Consolidate Patterns section -@test "prompt.md: has Consolidate Patterns section" { - run grep -q "Consolidate Patterns" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template has Codebase Patterns section -@test "prompt.md: mentions Codebase Patterns section" { - run grep -q "Codebase Patterns" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template has Update CLAUDE.md Files section -@test "prompt.md: has Update CLAUDE.md Files section" { - run grep -q "Update CLAUDE.md Files" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template has Quality Requirements section -@test "prompt.md: has Quality Requirements section" { - run grep -q "Quality Requirements" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template has Browser Testing section -@test "prompt.md: has Browser Testing section" { - run grep -q "Browser Testing" "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions status field -@test "prompt.md: mentions status field for user stories" { - run grep -q 'status' "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions startedAt field -@test "prompt.md: mentions startedAt timestamp field" { - run grep -q 'startedAt' "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions completedAt field -@test "prompt.md: mentions completedAt timestamp field" { - run grep -q 'completedAt' "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions in_progress status -@test "prompt.md: mentions in_progress status" { - run grep -q 'in_progress' "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions done status -@test "prompt.md: mentions done status" { - run grep -q '"done"' "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: prompt.md template mentions ISO timestamp -@test "prompt.md: mentions ISO timestamp" { - run grep -q 'ISO timestamp' "$PROMPT_TEMPLATE" - [ $status -eq 0 ] -} - -# Test: Script substitutes template variables -@test "ralph.sh: substitutes {{ITERATION}} and {{MAX}} in prompt" { - run grep 'sed "s/{{ITERATION}}/' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'sed.*{{MAX}}/' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script calls claude --print -@test "ralph.sh: calls claude --print" { - run grep -q 'claude --print' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script checks for COMPLETE promise -@test "ralph.sh: checks for COMPLETE" { - run grep -q 'COMPLETE' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script exits with 0 when COMPLETE detected -@test "ralph.sh: exits with 0 when COMPLETE detected" { - # Check that after detecting COMPLETE, script exits with 0 - run grep -A 5 'grep -q.*COMPLETE' "$RALPH_SCRIPT" - [[ "$output" == *"exit 0"* ]] -} - -# Test: Script iterates with proper loop -@test "ralph.sh: uses seq loop for iterations" { - run grep 'seq 1' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep '\$MAX_ITERATIONS' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script shows iteration progress -@test "ralph.sh: shows iteration progress" { - run grep 'iteration' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has cleanup trap -@test "ralph.sh: has cleanup trap" { - run grep -q "trap cleanup EXIT" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script cleanup function removes PID file -@test "ralph.sh: cleanup function removes PID file" { - run grep -A 2 "^cleanup()" "$RALPH_SCRIPT" - [[ "$output" == *"rm -f"* ]] - [[ "$output" == *"PID_FILE"* ]] -} - -# Test: Script sleeps between iterations to prevent API rate limiting -@test "ralph.sh: sleeps between iterations" { - run grep -q "sleep 2" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script creates log directory for iteration archives -@test "ralph.sh: creates log directory for iteration archives" { - run grep -q "mkdir -p \"\$LOG_DIR\"" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has LOG_DIR variable configured -@test "ralph.sh: has LOG_DIR variable" { - run grep -q 'LOG_DIR=' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'LOG_DIR="\$RALPH_DIR/logs"' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script writes iteration logs to archive directory -@test "ralph.sh: writes iteration logs to archive directory" { - run grep -q 'ITERATION_LOG=' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'ITERATION_LOG="\$LOG_DIR/iteration-' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script supports --dangerously-skip-permissions flag -@test "ralph.sh: supports --dangerously-skip-permissions flag" { - run grep -q 'dangerously-skip-permissions' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script passes --dangerously-skip-permissions to claude command -@test "ralph.sh: passes --dangerously-skip-permissions to claude" { - run grep 'claude.*--print.*--dangerously-skip-permissions' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Functional Test: cancel-ralph PID kill behavior -@test "cancel-ralph: kills ralph.sh process and removes PID file" { - # Skip this test on CI due to timing issues - skip "Test skipped due to timing sensitivity on CI runners" - - # Clean up any previous test state - cd "$TEST_GIT_DIR" - rm -rf .ralph - git checkout - 2>/dev/null || true - git branch -D ralph/* 2>/dev/null || true - - create_prd - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Start ralph.sh in background - bash "$RALPH_SCRIPT" 10 >/dev/null 2>&1 & - ralph_pid=$! - - # Wait for PID file to be created (poll with longer timeout for CI) - local count=0 - while [ ! -f .ralph/ralph.pid ] && [ $count -lt 50 ]; do - sleep 0.1 - count=$((count + 1)) - done - - # Give script time to fully initialize - sleep 0.5 - - # Verify PID file exists - [ -f .ralph/ralph.pid ] - - pid_from_file=$(cat .ralph/ralph.pid) - [ "$pid_from_file" = "$ralph_pid" ] - - # Verify process is running - kill -0 "$ralph_pid" 2>/dev/null - - # Kill the process (simulating cancel-ralph behavior) - kill "$ralph_pid" 2>/dev/null || true - sleep 0.2 - - # Verify process is dead - run ! kill -0 "$ralph_pid" 2>/dev/null - - # Remove PID file (as cancel-ralph would do) - rm -f .ralph/ralph.pid - - # Verify PID file is removed - [ ! -f .ralph/ralph.pid ] -} - -# Functional Test: ralph.sh iteration counting -@test "ralph.sh: iterates correct number of times" { - # Skip this test on CI due to timing issues with background processes - skip "Test skipped on CI due to timing sensitivity with mock claude" - - # Clean up any previous test state - cd "$TEST_GIT_DIR" - rm -rf .ralph - git checkout - 2>/dev/null || true - git branch -D ralph/* 2>/dev/null || true - - create_prd - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Create a mock claude that never returns COMPLETE - # This allows ralph.sh to run through all iterations - cat > "$MOCK_CLAUDE" <<'EOF' -#!/bin/bash -# Mock claude that never returns COMPLETE -if [[ "$*" == *"--print"* ]]; then - # Read from stdin and echo response without COMPLETE - cat >/dev/null - echo "Working on tasks..." - exit 0 -fi -echo "Mock claude command" -EOF - chmod +x "$MOCK_CLAUDE" - - # Run ralph.sh with 3 iterations, output to a temp file - local output_file="${TEST_TEMP_DIR}/ralph_output.txt" - bash "$RALPH_SCRIPT" 3 > "$output_file" 2>&1 & - ralph_pid=$! - - # Wait for all iterations to complete with much longer timeout for CI - local count=0 - while kill -0 "$ralph_pid" 2>/dev/null && [ $count -lt 60 ]; do - sleep 0.5 - count=$((count + 1)) - done - - # Wait for process to finish and flush output - wait $ralph_pid 2>/dev/null || true - sleep 0.5 - - # Read output from file, removing any null bytes - output=$(tr -d '\0' < "$output_file") - - # Verify we ran exactly 3 iterations - # The script should show "=== Ralph iteration 1/3 ===", "2/3", "3/3" - iteration_count=$(echo "$output" | grep -c "Ralph iteration" || true) - [ "$iteration_count" -eq 3 ] - - # Should show "reached max iterations" message - [[ "$output" == *"max iterations"* ]] -} - -# Functional Test: ralph.sh COMPLETE detection -@test "ralph.sh: detects COMPLETE and exits with 0" { - # Clean up any previous test state - cd "$TEST_GIT_DIR" - rm -rf .ralph - git checkout - 2>/dev/null || true - git branch -D ralph/* 2>/dev/null || true - - create_prd - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Create a mock claude that returns COMPLETE immediately - cat > "$MOCK_CLAUDE" <<'EOF' -#!/bin/bash -# Mock claude that returns COMPLETE -if [[ "$*" == *"--print"* ]]; then - # Read from stdin - cat >/dev/null - # Return COMPLETE promise - echo "COMPLETE" - exit 0 -fi -echo "Mock claude command" -EOF - chmod +x "$MOCK_CLAUDE" - - # Run ralph.sh with 10 iterations but expect it to exit early - run bash "$RALPH_SCRIPT" 10 2>&1 - - # Should exit with 0 (not 1 for max iterations) - [ $status -eq 0 ] - - # Should show completion message - [[ "$output" == *"Ralph completed at iteration"* ]] - - # Should show iteration 1 (should exit on first iteration) - [[ "$output" == *"iteration 1/10"* ]] - - # Should NOT show iteration 2 or beyond - ! [[ "$output" == *"iteration 2/"* ]] -} - -# Test: Script creates .last-branch file on first run -@test "ralph.sh: creates .last-branch file on first run" { - local test_branch="ralph/first-run" - create_prd "$test_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # .last-branch should exist and contain the branch name - [ -f .ralph/.last-branch ] - [ "$(cat .ralph/.last-branch)" = "$test_branch" ] -} - -# Test: Script archives previous run when branch changes -@test "ralph.sh: archives previous run when branch changes" { - local first_branch="ralph/first-feature" - local second_branch="ralph/second-feature" - - # First run - create_prd "$first_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run first time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Modify progress.txt so we can verify it was archived - echo "Some progress content" >> .ralph/progress.txt - - # Update PRD for second run with different branch - create_prd "$second_branch" - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Run second time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Archive should be created - [ -d .ralph/archive ] - - # Find the archive directory (format: YYYY-MM-DD-first-feature) - local archive_dir - archive_dir=$(find .ralph/archive -type d -name "*-first-feature" | head -1) - [ -n "$archive_dir" ] - - # Archived files should exist - [ -f "$archive_dir/prd.json" ] - [ -f "$archive_dir/progress.txt" ] - - # Archived progress.txt should contain our custom content - grep -q "Some progress content" "$archive_dir/progress.txt" - - # .last-branch should be updated to new branch - [ "$(cat .ralph/.last-branch)" = "$second_branch" ] - - # Current progress.txt should be reset (no custom content) - ! grep -q "Some progress content" .ralph/progress.txt -} - -# Test: Script does not archive when branch is the same -@test "ralph.sh: does not archive when branch is the same" { - local test_branch="ralph/same-branch" - create_prd "$test_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run first time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Modify progress.txt - echo "Custom progress" >> .ralph/progress.txt - - # Run second time with same branch - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Archive directory should NOT be created - [ ! -d .ralph/archive ] - - # Custom progress should still be there (not reset) - grep -q "Custom progress" .ralph/progress.txt -} - -# Test: Script handles archive directory name collision -@test "ralph.sh: handles archive directory name collision with timestamp" { - local first_branch="ralph/collision-test" - local second_branch="ralph/other-feature" - - # First run - create_prd "$first_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run first time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Manually create an archive directory with today's date to simulate collision - local today - today=$(date +"%Y-%m-%d") - mkdir -p ".ralph/archive/${today}-collision-test" - - # Update PRD for second run - create_prd "$second_branch" - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Run second time - should create a timestamped archive to avoid collision - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Should have at least one collision-test archive directory - local archive_count - archive_count=$(find .ralph/archive -type d -name "*-collision-test*" | wc -l) - [ "$archive_count" -ge 1 ] -} - -# Test: Archive removes ralph/ prefix from branch name -@test "ralph.sh: archive directory name removes ralph/ prefix" { - local first_branch="ralph/my-awesome-feature" - local second_branch="ralph/other-feature" - - # First run - create_prd "$first_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run first time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Update PRD for second run - create_prd "$second_branch" - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Run second time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Archive directory should be named with feature name, not full branch - local archive_dir - archive_dir=$(find .ralph/archive -type d -name "*-my-awesome-feature" | head -1) - [ -n "$archive_dir" ] - - # Should NOT contain ralph/ in the directory name - [[ "$archive_dir" != *"/ralph-my-awesome-feature" ]] -} - -# Test: Script handles null branchName in prd.json -@test "ralph.sh: handles null branchName without archiving" { - # Create PRD with null branchName - cat > "$TEST_RALPH_DIR/prd.json" < test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run with null branchName - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # .last-branch should contain "null" as written - [ -f .ralph/.last-branch ] - [ "$(cat .ralph/.last-branch)" = "null" ] -} - -# Test: Script validates prd.json is valid JSON -@test "ralph.sh: validates prd.json is valid JSON" { - cd "$TEST_GIT_DIR" - mkdir -p .ralph - - # Create invalid JSON file - echo "{ invalid json" > .ralph/prd.json - - run bash "$RALPH_SCRIPT" 2 - [ $status -ne 0 ] - [[ "$output" == *"not valid JSON"* ]] -} - -# Test: Script validates prd.json has required fields -@test "ralph.sh: validates prd.json has required fields" { - cd "$TEST_GIT_DIR" - mkdir -p .ralph - - # Create valid JSON but missing required field - echo '{"project": "test"}' > .ralph/prd.json - - run bash "$RALPH_SCRIPT" 2 - [ $status -ne 0 ] - [[ "$output" == *"missing required field"* ]] -} - -# Test: Script has proper cleanup on SIGINT -@test "ralph.sh: cleanup function removes PID file and shows exit info" { - run grep -A 5 "^cleanup()" "$RALPH_SCRIPT" - [[ "$output" == *"rm -f"* ]] - [[ "$output" == *"PID_FILE"* ]] -} - -# Test: Script has INT signal handler -@test "ralph.sh: has INT signal handler" { - run grep "trap.*INT" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has TERM signal handler -@test "ralph.sh: has TERM signal handler" { - run grep "trap.*TERM" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has show_progress_summary function -@test "ralph.sh: has show_progress_summary function" { - run grep "show_progress_summary()" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script calls show_progress_summary after iteration -@test "ralph.sh: calls show_progress_summary after iteration" { - run grep -A 3 "Iteration.*finished, continuing" "$RALPH_SCRIPT" - [[ "$output" == *"show_progress_summary"* ]] -} - -# Test: Script calls show_progress_summary on completion -@test "ralph.sh: calls show_progress_summary on completion" { - run grep -B 3 -A 3 "Ralph completed" "$RALPH_SCRIPT" - [[ "$output" == *"show_progress_summary"* ]] -} - -# === New State File Tests === - -# Test: Script creates guardrails.md if missing -@test "ralph.sh: creates guardrails.md if missing" { - create_prd - # Don't create guardrails.md - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # guardrails.md should be created - [ -f .ralph/guardrails.md ] - grep -q "# Ralph Guardrails" .ralph/guardrails.md -} - -# Test: Script creates activity.log if missing -@test "ralph.sh: creates activity.log if missing" { - create_prd - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # activity.log should be created - [ -f .ralph/activity.log ] -} - -# Test: Script creates errors.log if missing -@test "ralph.sh: creates errors.log if missing" { - create_prd - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # errors.log should be created - [ -f .ralph/errors.log ] -} - -# Test: Script has guardrails file variable -@test "ralph.sh: has GUARDRAILS_FILE variable" { - run grep -q 'GUARDRAILS_FILE=' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'GUARDRAILS_FILE="\$RALPH_DIR/guardrails.md"' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has activity log file variable -@test "ralph.sh: has ACTIVITY_LOG variable" { - run grep -q 'ACTIVITY_LOG=' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'ACTIVITY_LOG="\$RALPH_DIR/activity.log"' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has errors log file variable -@test "ralph.sh: has ERRORS_LOG variable" { - run grep -q 'ERRORS_LOG=' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'ERRORS_LOG="\$RALPH_DIR/errors.log"' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has ensure_guardrails_file function -@test "ralph.sh: has ensure_guardrails_file function" { - run grep "ensure_guardrails_file()" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: guardrails.md has proper header -@test "ralph.sh: guardrails.md has proper header when created" { - create_prd - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Check guardrails.md has proper structure - grep -q "# Ralph Guardrails" .ralph/guardrails.md - grep -q "Lessons Learned" .ralph/guardrails.md -} - -# Test: Script archives guardrails.md when branch changes -@test "ralph.sh: archives guardrails.md when branch changes" { - local first_branch="ralph/first-feature" - local second_branch="ralph/second-feature" - - # First run - create_prd "$first_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run first time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Add custom content to guardrails.md - echo "## Custom guardrail" >> .ralph/guardrails.md - - # Update PRD for second run with different branch - create_prd "$second_branch" - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Run second time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Archive should contain guardrails.md - local archive_dir - archive_dir=$(find .ralph/archive -type d -name "*-first-feature" | head -1) - [ -f "$archive_dir/guardrails.md" ] - - # Archived guardrails.md should contain our custom content - grep -q "Custom guardrail" "$archive_dir/guardrails.md" -} - -# Test: Script archives activity.log when branch changes -@test "ralph.sh: archives activity.log when branch changes" { - local first_branch="ralph/first-feature" - local second_branch="ralph/second-feature" - - # First run - create_prd "$first_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run first time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Add custom content to activity.log - echo "2026-02-09T12:00:00Z - Iteration 1 started" >> .ralph/activity.log - - # Update PRD for second run with different branch - create_prd "$second_branch" - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Run second time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Archive should contain activity.log - local archive_dir - archive_dir=$(find .ralph/archive -type d -name "*-first-feature" | head -1) - [ -f "$archive_dir/activity.log" ] - - # Archived activity.log should contain our custom content - grep -q "2026-02-09T12:00:00Z - Iteration 1 started" "$archive_dir/activity.log" -} - -# Test: Script archives errors.log when branch changes -@test "ralph.sh: archives errors.log when branch changes" { - local first_branch="ralph/first-feature" - local second_branch="ralph/second-feature" - - # First run - create_prd "$first_branch" - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run first time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Add custom content to errors.log - echo "2026-02-09T12:00:00Z - ERROR: Test failure" >> .ralph/errors.log - - # Update PRD for second run with different branch - create_prd "$second_branch" - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Run second time - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Archive should contain errors.log - local archive_dir - archive_dir=$(find .ralph/archive -type d -name "*-first-feature" | head -1) - [ -f "$archive_dir/errors.log" ] - - # Archived errors.log should contain our custom content - grep -q "2026-02-09T12:00:00Z - ERROR: Test failure" "$archive_dir/errors.log" -} - -# Test: Script logs successful iteration to activity.log -@test "ralph.sh: logs successful iteration to activity.log" { - create_prd - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it after it starts - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # activity.log should exist and have a success entry - [ -f .ralph/activity.log ] - # Check for SUCCESS status in the log - grep -q "STATUS=SUCCESS" .ralph/activity.log -} - -# Test: Script logs failed iteration with ERROR status -@test "ralph.sh: logs failed iteration with ERROR status" { - create_prd - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Create a mock claude that exits with code 1 (transient error) - cat > "$MOCK_CLAUDE" <<'EOF' -#!/bin/bash -# Mock claude that fails with exit code 1 -if [[ "$*" == *"--print"* ]]; then - cat >/dev/null - exit 1 -fi -echo "Mock claude command" -EOF - chmod +x "$MOCK_CLAUDE" - - # Run ralph.sh - it should continue after exit code 1 - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # activity.log should have ERROR status entry - [ -f .ralph/activity.log ] - grep -q "STATUS=ERROR" .ralph/activity.log -} - -# Test: Script logs iteration with START and END timestamps -@test "ralph.sh: logs iteration with START and END timestamps" { - create_prd - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # activity.log should have START and END timestamps - [ -f .ralph/activity.log ] - # Check for START= timestamp format - grep -q "START=" .ralph/activity.log - # Check for END= timestamp format - grep -q "END=" .ralph/activity.log -} - -# Test: Script logs Claude command failure to errors.log -@test "ralph.sh: logs Claude command failure to errors.log" { - create_prd - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/prd.json" .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Create a mock claude that exits with code 1 - cat > "$MOCK_CLAUDE" <<'EOF' -#!/bin/bash -# Mock claude that fails with exit code 1 -if [[ "$*" == *"--print"* ]]; then - cat >/dev/null - exit 1 -fi -echo "Mock claude command" -EOF - chmod +x "$MOCK_CLAUDE" - - # Run ralph.sh - it should continue after exit code 1 - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # errors.log should have an entry about the failure - [ -f .ralph/errors.log ] - # Check for error log entry with exit code - grep -q "failed with exit code" .ralph/errors.log -} - -# Test: Script uses get_guardrails_template helper -@test "ralph.sh: has get_guardrails_template helper function" { - run grep "get_guardrails_template()" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: ensure_guardrails_file calls get_guardrails_template -@test "ralph.sh: ensure_guardrails_file uses get_guardrails_template" { - run grep -A 10 "^ensure_guardrails_file()" "$RALPH_SCRIPT" - [[ "$output" == *"get_guardrails_template"* ]] -} - -# Test: Script has inline comments for new state file variables -@test "ralph.sh: has inline comments for state file variables" { - # Check for comment explaining GUARDRAILS_FILE - run grep "# Guardrails file" "$RALPH_SCRIPT" - [ $status -eq 0 ] - # Check for comment explaining ACTIVITY_LOG - run grep "# Activity log" "$RALPH_SCRIPT" - [ $status -eq 0 ] - # Check for comment explaining ERRORS_LOG - run grep "# Errors log" "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# === Configuration File Tests === - -# Test: Script sources .agents/ralph/config.sh if it exists -@test "ralph.sh: sources .agents/ralph/config.sh if it exists" { - run grep -q '\.agents/ralph/config\.sh' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep -q 'source.*"\$CONFIG_FILE"' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script has STALE_SECONDS variable with default -@test "ralph.sh: has STALE_SECONDS variable with default" { - run grep -q 'STALE_SECONDS=' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'STALE_SECONDS=' "$RALPH_SCRIPT" - [[ "$output" == *"86400"* ]] # Default should be 86400 (24 hours) -} - -# Test: Script sources config file safely (checks existence first) -@test "ralph.sh: sources config file safely with existence check" { - run grep -B 2 'source.*"\$CONFIG_FILE"' "$RALPH_SCRIPT" - [[ "$output" == *"[ -f "* ]] || [[ "$output" == *"[[ -f "* ]] -} - -# Test: Config file sourcing uses correct path -@test "ralph.sh: config file path is .agents/ralph/config.sh" { - run grep '\.agents/ralph/config\.sh' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# === Template Hierarchy Tests === - -# Test: Script has custom prompt template path variable -@test "ralph.sh: has CUSTOM_PROMPT_TEMPLATE variable for .agents/ralph/prompts/loop.md" { - run grep -q 'CUSTOM_PROMPT_TEMPLATE=' "$RALPH_SCRIPT" - [ $status -eq 0 ] - run grep 'CUSTOM_PROMPT_TEMPLATE="\.agents/ralph/prompts/loop\.md"' "$RALPH_SCRIPT" - [ $status -eq 0 ] -} - -# Test: Script falls back to default prompt template -@test "ralph.sh: falls back to default prompt.md when custom doesn't exist" { - run grep -A 5 'CUSTOM_PROMPT_TEMPLATE=' "$RALPH_SCRIPT" - # Should check if custom template exists, otherwise use default - [[ "$output" == *"PROMPT_TEMPLATE"* ]] -} - -# Test: Script uses custom prompt template when it exists -@test "ralph.sh: uses custom prompt template from .agents/ralph/prompts/loop.md" { - # Check that script has logic to use custom template - run grep -A 10 'CUSTOM_PROMPT_TEMPLATE=' "$RALPH_SCRIPT" - [[ "$output" == *"[ -f"* ]] || [[ "$output" == *"[[ -f"* ]] -} - -# Functional Test: Script uses default template when custom doesn't exist -@test "ralph.sh: functional - uses default template when custom doesn't exist" { - create_prd - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Ensure custom template doesn't exist - rm -rf .agents/ralph/prompts/ - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Script should have run successfully using default template - [ ! -f .agents/ralph/prompts/loop.md ] -} - -# Functional Test: Script uses custom template when it exists -@test "ralph.sh: functional - uses custom template when it exists" { - create_prd - create_progress - - cd "$TEST_GIT_DIR" - mkdir -p .ralph - cp "$TEST_RALPH_DIR/"* .ralph/ - - # Create an initial commit - echo "test" > test.txt - git add test.txt - git commit -q -m "Initial commit" 2>/dev/null || true - - # Create custom prompt template - mkdir -p .agents/ralph/prompts - cat > .agents/ralph/prompts/loop.md <<'EOF' -# Custom Prompt Template -This is a custom template for testing. - -Iteration {{ITERATION}} of {{MAX}} -EOF - - # Run ralph.sh in background and kill it - bash "$RALPH_SCRIPT" 1 >/dev/null 2>&1 & - ralph_pid=$! - sleep 0.5 - kill $ralph_pid 2>/dev/null || true - wait $ralph_pid 2>/dev/null || true - - # Custom template should still exist and be used - [ -f .agents/ralph/prompts/loop.md ] - grep -q "Custom Prompt Template" .agents/ralph/prompts/loop.md -} diff --git a/tests/run-all-tests.sh b/tests/run-all-tests.sh index 86b1d297..38f6e0b6 100755 --- a/tests/run-all-tests.sh +++ b/tests/run-all-tests.sh @@ -48,7 +48,7 @@ run_consolidated_tests() { echo "========================================" # Run tests for each subdirectory in tests/ - local test_dirs=("integration" "skills" "performance" "me" "jira" "git-guard") + local test_dirs=("integration" "skills" "performance" "jira" "git-guard" "databricks-devtools" "handoff") for dir in "${test_dirs[@]}"; do if [ -d "${SCRIPT_DIR}/${dir}" ]; then diff --git a/tests/suggest-compacting/suggest-compacting.bats b/tests/suggest-compacting/suggest-compacting.bats deleted file mode 100644 index 6e3cfebf..00000000 --- a/tests/suggest-compacting/suggest-compacting.bats +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env bats - -setup() { - # Setup test environment - export TEST_SESSION_ID="test-session-123" - export TEST_STATE_DIR="$HOME/.claude/suggest-compacting" - export TEST_STATE_FILE="$TEST_STATE_DIR/tool-count-$TEST_SESSION_ID.txt" - export CLAUDE_PLUGIN_ROOT="$PWD/plugins/suggest-compacting" - - # Clean up any existing test state - rm -f "$TEST_STATE_FILE" 2>/dev/null || true -} - -teardown() { - # Clean up test state - rm -f "$TEST_STATE_FILE" 2>/dev/null || true -} - -@test "suggest-compacting: plugin.json exists" { - [ -f "$CLAUDE_PLUGIN_ROOT/.claude-plugin/plugin.json" ] -} - -@test "suggest-compacting: hooks.json exists" { - [ -f "$CLAUDE_PLUGIN_ROOT/hooks/hooks.json" ] -} - -@test "suggest-compacting: hooks.json has SessionStart hook" { - run jq -e '.hooks.SessionStart' "$CLAUDE_PLUGIN_ROOT/hooks/hooks.json" - [ "$status" -eq 0 ] -} - -@test "suggest-compacting: hooks.json has PreToolUse hook" { - run jq -e '.hooks.PreToolUse' "$CLAUDE_PLUGIN_ROOT/hooks/hooks.json" - [ "$status" -eq 0 ] -} - -@test "suggest-compacting: SessionStart hook uses tsx" { - run jq -r '.hooks.SessionStart[0].hooks[0].command' "$CLAUDE_PLUGIN_ROOT/hooks/hooks.json" - echo "$output" | grep -q "session-start.ts" -} - -@test "suggest-compacting: PreToolUse hook uses tsx" { - run jq -r '.hooks.PreToolUse[0].hooks[0].command' "$CLAUDE_PLUGIN_ROOT/hooks/hooks.json" - echo "$output" | grep -q "auto-compact.ts" -} - -@test "suggest-compacting: TypeScript source files exist" { - [ -f "$CLAUDE_PLUGIN_ROOT/src/lib/state.ts" ] - [ -f "$CLAUDE_PLUGIN_ROOT/src/session-start.ts" ] - [ -f "$CLAUDE_PLUGIN_ROOT/src/auto-compact.ts" ] -} - -@test "suggest-compacting: hooks are executable" { - [ -x "$CLAUDE_PLUGIN_ROOT/src/session-start.ts" ] - [ -x "$CLAUDE_PLUGIN_ROOT/src/auto-compact.ts" ] -} - -@test "suggest-compacting: TypeScript config exists" { - [ -f "$CLAUDE_PLUGIN_ROOT/tsconfig.json" ] -} - -@test "suggest-compacting: Jest config removed (no longer needed)" { - [ ! -f "$CLAUDE_PLUGIN_ROOT/jest.config.cjs" ] -} - -@test "suggest-compacting: unit tests removed (no longer needed)" { - [ ! -d "$CLAUDE_PLUGIN_ROOT/tests/unit" ] -} - -@test "suggest-compacting: old Bash hooks are removed" { - [ ! -f "$CLAUDE_PLUGIN_ROOT/hooks/auto-compact.sh" ] - [ ! -f "$CLAUDE_PLUGIN_ROOT/hooks/session-start-hook.sh" ] - [ ! -f "$CLAUDE_PLUGIN_ROOT/hooks/lib/state.sh" ] -} - -@test "suggest-compacting: auto-compact hook increments counter" { - # Create temp input file - local tmpinput - tmpinput=$(mktemp) - echo "{\"tool_name\":\"Read\",\"session_id\":\"$TEST_SESSION_ID\"}" > "$tmpinput" - - # Run the hook - cat "$tmpinput" | npx tsx "$CLAUDE_PLUGIN_ROOT/src/auto-compact.ts" > /dev/null 2>&1 - rm -f "$tmpinput" - - # Check that state file was created - [ -f "$TEST_STATE_FILE" ] - - # Check that counter was incremented - local count - count=$(cat "$TEST_STATE_FILE") - [ "$count" -eq 1 ] -} - -@test "suggest-compacting: auto-compact hook suggests at threshold" { - # Set COMPACT_THRESHOLD to 3 for testing - export COMPACT_THRESHOLD=3 - - # Create temp input file - local tmpinput - tmpinput=$(mktemp) - echo "{\"tool_name\":\"Read\",\"session_id\":\"$TEST_SESSION_ID\"}" > "$tmpinput" - - # Reset counter - rm -f "$TEST_STATE_FILE" - - # Run the hook 2 times (count becomes 2) - local i=1 - while [ $i -le 2 ]; do - cat "$tmpinput" | npx tsx "$CLAUDE_PLUGIN_ROOT/src/auto-compact.ts" > /dev/null 2>&1 - i=$((i + 1)) - done - - # Third call should trigger suggestion (count == 3 == threshold) - local output - output=$(cat "$tmpinput" | npx tsx "$CLAUDE_PLUGIN_ROOT/src/auto-compact.ts" 2>&1) - rm -f "$tmpinput" - - echo "$output" | grep -q "Suggestion" - echo "$output" | grep -q "tool calls" -} - -@test "suggest-compacting: session-start hook validates session ID" { - # Create temp input files - local tmpinput - tmpinput=$(mktemp) - local tmpinvalid - tmpinvalid=$(mktemp) - - echo "{\"session_id\":\"valid-session-123\",\"transcript_path\":\"/tmp/test.json\"}" > "$tmpinput" - echo "{\"session_id\":\"../../../etc/passwd\",\"transcript_path\":\"/tmp/test.json\"}" > "$tmpinvalid" - - # Test with valid session ID (no env file, should succeed silently) - run bash -c "cat '$tmpinput' | npx tsx $CLAUDE_PLUGIN_ROOT/src/session-start.ts" - [ "$status" -eq 0 ] - - # Test with invalid session ID (should fail) - run bash -c "cat '$tmpinvalid' | npx tsx $CLAUDE_PLUGIN_ROOT/src/session-start.ts" - [ "$status" -ne 0 ] - - rm -f "$tmpinput" "$tmpinvalid" -} - -@test "suggest-compacting: package.json exists without Jest scripts" { - [ -f "$CLAUDE_PLUGIN_ROOT/package.json" ] - - # Verify Jest is not in dependencies - run jq -r '.devDependencies.jest' "$CLAUDE_PLUGIN_ROOT/package.json" - [ "$output" = "null" ] - - # Verify TypeScript and tsx are present - run jq -r '.devDependencies.typescript' "$CLAUDE_PLUGIN_ROOT/package.json" - [ "$output" != "null" ] - - run jq -r '.devDependencies.tsx' "$CLAUDE_PLUGIN_ROOT/package.json" - [ "$output" != "null" ] -} diff --git a/tests/validate_plugin_manifest.bats b/tests/validate_plugin_manifest.bats index b64e4217..9967b968 100644 --- a/tests/validate_plugin_manifest.bats +++ b/tests/validate_plugin_manifest.bats @@ -118,14 +118,31 @@ load helpers/fixture_factory } @test "all real plugin manifests are valid" { - # Find all plugin.json files in the project - local manifest_files - manifest_files=$(find "$PROJECT_ROOT/plugins" -name "plugin.json" -type f 2>/dev/null) + local manifest_files="" + local found=0 - [ -n "$manifest_files" ] || skip "No plugin.json files found" + # Check root canonical plugin + local root_manifest="$PROJECT_ROOT/.claude-plugin/plugin.json" + if [ -f "$root_manifest" ]; then + manifest_files="$root_manifest"$'\n' + found=$((found + 1)) + fi + + # Find all plugin.json files in plugins directory + local plugins_manifests + plugins_manifests=$(find "$PROJECT_ROOT/plugins" -name "plugin.json" -type f 2>/dev/null) + + if [ -n "$plugins_manifests" ]; then + manifest_files="${manifest_files}${plugins_manifests}" + found=1 + fi + + [ "$found" -gt 0 ] || skip "No plugin.json files found" # Validate each manifest while IFS= read -r manifest_file; do + [ -z "$manifest_file" ] && continue + # Check if JSON is valid run validate_json "$manifest_file" [ "$status" -eq 0 ]