diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..7ffc088 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,25 @@ +{ + "name": "hartbrook-plugins", + "owner": { + "name": "HartBrook" + }, + "metadata": { + "description": "Claude Code plugins by HartBrook" + }, + "plugins": [ + { + "name": "look", + "source": "./src", + "description": "Sequential code review with fresh agent contexts. Runs multiple independent review passes to catch more issues.", + "version": "0.2.0", + "author": { "name": "HartBrook" }, + "repository": "https://github.com/HartBrook/lookagain", + "license": "MIT", + "keywords": ["code-review", "quality", "iterative", "multi-pass"], + "commands": ["./commands/again.md", "./commands/tidy.md"], + "agents": ["./agents/lookagain-reviewer.md"], + "skills": ["./skills/lookagain-output-format"], + "strict": false + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index c725960..881056e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2026-01-28 + +### Added + +- `/look:tidy` command for pruning old review runs +- Marketplace manifest (`.claude-plugin/marketplace.json`) for plugin discovery +- Target scope selection: `staged`, `commit`, `branch`, or a specific path +- Model selection: `thorough`, `balanced` (Sonnet), `fast` (Haiku) +- Marketplace install/test workflow in CONTRIBUTING guide + +### Changed + +- Streamlined reviewer agent prompt for clarity and focus +- Restructured `/look:again` command with clearer argument handling +- Expanded README with target scopes, model options, and tidy usage +- Hardened `test.sh` with marketplace manifest validation + ## [0.1.0] - 2026-01-26 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c5f3d7..3c6541b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,12 @@ ``` lookagain/ +├── .claude-plugin/ +│ └── marketplace.json # Marketplace manifest ├── src/ │ ├── commands/ # Plugin commands -│ │ └── again.md # Main orchestrator +│ │ ├── again.md # Main orchestrator +│ │ └── tidy.md # Tidy old review runs │ ├── agents/ # Subagent definitions │ │ └── lookagain-reviewer.md │ ├── skills/ # Output format specs @@ -50,6 +53,23 @@ make help `make dev` builds the plugin and starts a new Claude Code session with it loaded. Test with `/look:again`. +### Testing via Marketplace (local) + +You can also test the plugin through the marketplace install flow, which is closer to what end users experience: + +```bash +# First time: add the local marketplace +/plugin marketplace add ./ + +# Install the plugin +/plugin install look@hartbrook-plugins + +# After making changes, reinstall to pick them up +/plugin uninstall look@hartbrook-plugins +/plugin marketplace update hartbrook-plugins +/plugin install look@hartbrook-plugins +``` + ### Making Changes 1. Edit files in `src/` @@ -63,6 +83,8 @@ make help - **[src/agents/lookagain-reviewer.md](src/agents/lookagain-reviewer.md)**: Reviewer subagent. Defines how individual review passes work. - **[src/skills/lookagain-output-format/SKILL.md](src/skills/lookagain-output-format/SKILL.md)**: JSON output format specification. - **[src/dot-claude-plugin/plugin.json](src/dot-claude-plugin/plugin.json)**: Plugin metadata and version. +- **[src/commands/tidy.md](src/commands/tidy.md)**: Tidy command for pruning old review runs. +- **[.claude-plugin/marketplace.json](.claude-plugin/marketplace.json)**: Marketplace manifest for plugin discovery and installation. ## Pull Requests @@ -75,4 +97,4 @@ make help ## Versioning -Update the version in `src/dot-claude-plugin/plugin.json` when making releases. +Update the version in `src/dot-claude-plugin/plugin.json` when making releases. The marketplace entry in `.claude-plugin/marketplace.json` also has a `version` field — keep both in sync. The test suite (`make test`) validates that commands, agents, and skills match between the two files. diff --git a/README.md b/README.md index f2e53e5..fb6a1c1 100644 --- a/README.md +++ b/README.md @@ -34,35 +34,64 @@ A single code review pass catches ~60-70% of issues. Running multiple independen ## Installation ```bash -# Add the marketplace (if not already added) +# Add the marketplace /plugin marketplace add HartBrook/lookagain # Install the plugin -/plugin install lookagain +/plugin install look@hartbrook-plugins ``` ## Usage ```bash -# Basic: 3 review passes with auto-fix for must_fix issues +# Review staged changes (default) /look:again +# Review the last commit +/look:again target=commit + +# Review all changes on the current branch +/look:again target=branch + +# Review a specific directory +/look:again target=src/auth + # More passes for critical code /look:again passes=5 -# Review specific directory -/look:again target=src/auth +# Use a faster, cheaper model for reviewers +/look:again model=fast + +# Balanced cost/quality +/look:again model=balanced # Disable auto-fix (review only) /look:again auto-fix=false -# Increase max passes for stubborn issues -/look:again passes=3 max-passes=10 +# Combine options +/look:again target=branch passes=5 model=fast ``` +### Target Scopes + +| Target | What gets reviewed | +| --- | --- | +| `staged` (default) | Files in the git staging area | +| `commit` | Files changed in the last commit | +| `branch` | All changes on the current branch vs base | +| `` | Files in the given directory or path | + +### Model Options + +| Model | Engine | Best for | +| --- | --- | --- | +| `thorough` (default) | Inherits current model | Critical code, security-sensitive reviews | +| `balanced` | Sonnet | Good balance of cost and quality | +| `fast` | Haiku | Quick checks, large codebases, cost-conscious usage | + ## How It Works -1. **Pass 1**: Fresh subagent reviews code, outputs findings as JSON +1. **Pass 1**: Fresh subagent reviews scoped changes, outputs findings as JSON 2. **Fix**: Must-fix issues are automatically fixed (if auto-fix enabled) 3. **Pass 2**: New fresh subagent reviews (doesn't know what Pass 1 found) 4. **Fix**: Any new must-fix issues are fixed @@ -74,20 +103,34 @@ Issues found by multiple passes have higher confidence scores. ## Output -Results are saved to `.lookagain/`: +Each run saves results to a timestamped directory `.lookagain//`: - `aggregate.md` - Human-readable summary - `aggregate.json` - Machine-readable findings - `pass-N.json` - Raw output from each pass -## Configuration +Previous runs are preserved. Use `/look:tidy` to prune old results: + +```bash +# Remove runs older than 1 day (default) +/look:tidy -Default behavior: +# Keep last 3 days +/look:tidy keep=3 + +# Remove all runs +/look:tidy all=true +``` + +## Configuration -- 3 review passes -- Auto-fix must_fix issues -- Maximum 7 passes if must_fix issues persist -- Reviews current directory +| Argument | Default | Description | +| --- | --- | --- | +| `passes` | `3` | Number of review passes | +| `target` | `staged` | What to review: `staged`, `commit`, `branch`, or a path | +| `auto-fix` | `true` | Auto-fix `must_fix` issues between passes | +| `model` | `thorough` | Reviewer model: `fast`, `balanced`, `thorough` | +| `max-passes` | `7` | Max passes if `must_fix` issues persist | ## Severity Levels diff --git a/scripts/package.sh b/scripts/package.sh index 82d7d06..44940b3 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -8,8 +8,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" DIST_DIR="$PROJECT_ROOT/dist" +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is required but not found" + exit 1 +fi + # Get version from plugin.json -VERSION=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT/src/dot-claude-plugin/plugin.json'))['version'])") +VERSION=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['version'])" "$PROJECT_ROOT/src/dot-claude-plugin/plugin.json") if [[ -z "$VERSION" ]]; then echo "Error: Could not extract version from plugin.json" @@ -25,6 +30,8 @@ mkdir -p "$DIST_DIR/lookagain" # Copy and rename directories cp -r "$PROJECT_ROOT/src/dot-claude" "$DIST_DIR/lookagain/.claude" cp -r "$PROJECT_ROOT/src/dot-claude-plugin" "$DIST_DIR/lookagain/.claude-plugin" +# Overlay marketplace.json from repo root into the dist .claude-plugin/ directory +cp "$PROJECT_ROOT/.claude-plugin/marketplace.json" "$DIST_DIR/lookagain/.claude-plugin/" cp -r "$PROJECT_ROOT/src/commands" "$DIST_DIR/lookagain/commands" cp -r "$PROJECT_ROOT/src/agents" "$DIST_DIR/lookagain/agents" cp -r "$PROJECT_ROOT/src/skills" "$DIST_DIR/lookagain/skills" diff --git a/scripts/test.sh b/scripts/test.sh index 3d2ce12..9a65e0b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -74,7 +74,7 @@ test_plugin_json() { local pjson="$PROJECT_ROOT/src/dot-claude-plugin/plugin.json" # Valid JSON - if python3 -c "import json; json.load(open('$pjson'))" 2>/dev/null; then + if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$pjson" 2>/dev/null; then pass "plugin.json is valid JSON" else fail "plugin.json is not valid JSON" @@ -89,9 +89,9 @@ test_plugin_json() { # Version matches semver if python3 -c " import json, re, sys -v = json.load(open('$pjson'))['version'] +v = json.load(open(sys.argv[1]))['version'] sys.exit(0 if re.match(r'^\d+\.\d+\.\d+$', v) else 1) -" 2>/dev/null; then +" "$pjson" 2>/dev/null; then pass "version is valid semver" else fail "version is not valid semver" @@ -100,9 +100,9 @@ sys.exit(0 if re.match(r'^\d+\.\d+\.\d+$', v) else 1) # Author has name if python3 -c " import json, sys -d = json.load(open('$pjson')) +d = json.load(open(sys.argv[1])) sys.exit(0 if isinstance(d.get('author'), dict) and 'name' in d['author'] else 1) -" 2>/dev/null; then +" "$pjson" 2>/dev/null; then pass "author.name present" else fail "author must be an object with name field" @@ -112,9 +112,11 @@ sys.exit(0 if isinstance(d.get('author'), dict) and 'name' in d['author'] else 1 test_required_files() { check_file "src/dot-claude-plugin/plugin.json" check_file "src/commands/again.md" + check_file "src/commands/tidy.md" check_file "src/agents/lookagain-reviewer.md" check_file "src/skills/lookagain-output-format/SKILL.md" check_file "src/dot-claude/settings.local.json" + check_file ".claude-plugin/marketplace.json" check_file "README.md" check_file "LICENSE" check_file "CHANGELOG.md" @@ -122,6 +124,7 @@ test_required_files() { test_frontmatter() { check_frontmatter "$PROJECT_ROOT/src/commands/again.md" name description + check_frontmatter "$PROJECT_ROOT/src/commands/tidy.md" name description check_frontmatter "$PROJECT_ROOT/src/agents/lookagain-reviewer.md" name description tools check_frontmatter "$PROJECT_ROOT/src/skills/lookagain-output-format/SKILL.md" name description } @@ -137,7 +140,7 @@ test_cross_references() { else fail "command $cmd not found at src/${cmd#./}" fi - done < <(python3 -c "import json; [print(c) for c in json.load(open('$pjson')).get('commands', [])]") + done < <(python3 -c "import json,sys; [print(c) for c in json.load(open(sys.argv[1])).get('commands', [])]" "$pjson") # Agents resolve while IFS= read -r agent; do @@ -147,7 +150,7 @@ test_cross_references() { else fail "agent $agent not found at src/${agent#./}" fi - done < <(python3 -c "import json; [print(a) for a in json.load(open('$pjson')).get('agents', [])]") + done < <(python3 -c "import json,sys; [print(a) for a in json.load(open(sys.argv[1])).get('agents', [])]" "$pjson") # Skills resolve with SKILL.md while IFS= read -r skill; do @@ -157,7 +160,7 @@ test_cross_references() { else fail "skill $skill not found or missing SKILL.md" fi - done < <(python3 -c "import json; [print(s) for s in json.load(open('$pjson')).get('skills', [])]") + done < <(python3 -c "import json,sys; [print(s) for s in json.load(open(sys.argv[1])).get('skills', [])]" "$pjson") } test_build() { @@ -180,13 +183,19 @@ test_build() { fail "dist/.claude-plugin/ missing" fi - if python3 -c "import json; json.load(open('$dist/.claude-plugin/plugin.json'))" 2>/dev/null; then + if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$dist/.claude-plugin/plugin.json" 2>/dev/null; then pass "dist plugin.json is valid JSON" else fail "dist plugin.json is invalid" fi - for f in commands/again.md agents/lookagain-reviewer.md skills/lookagain-output-format/SKILL.md README.md; do + if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$dist/.claude-plugin/marketplace.json" 2>/dev/null; then + pass "dist marketplace.json is valid JSON" + else + fail "dist marketplace.json is invalid" + fi + + for f in commands/again.md commands/tidy.md agents/lookagain-reviewer.md skills/lookagain-output-format/SKILL.md README.md; do if [[ -f "$dist/$f" ]]; then pass "dist/$f exists" else @@ -196,7 +205,7 @@ test_build() { # Zip exists with correct version local version - version=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT/src/dot-claude-plugin/plugin.json'))['version'])") + version=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['version'])" "$PROJECT_ROOT/src/dot-claude-plugin/plugin.json") if [[ -f "$PROJECT_ROOT/dist/lookagain-v${version}.zip" ]]; then pass "zip archive lookagain-v${version}.zip exists" else @@ -207,7 +216,7 @@ test_build() { test_settings() { local settings="$PROJECT_ROOT/src/dot-claude/settings.local.json" - if python3 -c "import json; json.load(open('$settings'))" 2>/dev/null; then + if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$settings" 2>/dev/null; then pass "settings.local.json is valid JSON" else fail "settings.local.json is not valid JSON" @@ -216,15 +225,86 @@ test_settings() { if python3 -c " import json, sys -d = json.load(open('$settings')) +d = json.load(open(sys.argv[1])) sys.exit(0 if isinstance(d.get('permissions', {}).get('allow'), list) else 1) -" 2>/dev/null; then +" "$settings" 2>/dev/null; then pass "permissions.allow is an array" else fail "permissions.allow missing or not an array" fi } +test_marketplace() { + local mjson="$PROJECT_ROOT/.claude-plugin/marketplace.json" + + # Valid JSON + if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$mjson" 2>/dev/null; then + pass "marketplace.json is valid JSON" + else + fail "marketplace.json is not valid JSON" + return + fi + + # Required fields + for field in name owner plugins; do + check_json_field "$mjson" "$field" + done + + # Owner has name + if python3 -c " +import json, sys +d = json.load(open(sys.argv[1])) +sys.exit(0 if isinstance(d.get('owner'), dict) and 'name' in d['owner'] else 1) +" "$mjson" 2>/dev/null; then + pass "owner.name present" + else + fail "owner must be an object with name field" + fi + + # Each plugin has name and source + if python3 -c " +import json, sys +d = json.load(open(sys.argv[1])) +plugins = d.get('plugins', []) +if not plugins: + sys.exit(1) +for p in plugins: + if 'name' not in p or 'source' not in p: + sys.exit(1) +" "$mjson" 2>/dev/null; then + pass "all plugins have name and source" + else + fail "plugins must each have name and source" + fi + + # Version matches plugin.json + local pjson="$PROJECT_ROOT/src/dot-claude-plugin/plugin.json" + if python3 -c " +import json, sys +pv = json.load(open(sys.argv[1]))['version'] +mv = json.load(open(sys.argv[2]))['plugins'][0].get('version', '') +sys.exit(0 if pv == mv else 1) +" "$pjson" "$mjson" 2>/dev/null; then + pass "version in sync between plugin.json and marketplace.json" + else + fail "version mismatch between plugin.json and marketplace.json" + fi + + # Commands, agents, skills match plugin.json + for field in commands agents skills; do + if python3 -c " +import json, sys +p = sorted(json.load(open(sys.argv[1])).get(sys.argv[3], [])) +m = sorted(json.load(open(sys.argv[2]))['plugins'][0].get(sys.argv[3], [])) +sys.exit(0 if p == m else 1) +" "$pjson" "$mjson" "$field" 2>/dev/null; then + pass "$field in sync between plugin.json and marketplace.json" + else + fail "$field mismatch between plugin.json and marketplace.json" + fi + done +} + # ============================================================ # Run # ============================================================ @@ -252,6 +332,10 @@ echo "--- settings ---" test_settings echo "" +echo "--- marketplace ---" +test_marketplace +echo "" + echo "--- build ---" test_build echo "" diff --git a/src/agents/lookagain-reviewer.md b/src/agents/lookagain-reviewer.md index e60d398..2f4b016 100644 --- a/src/agents/lookagain-reviewer.md +++ b/src/agents/lookagain-reviewer.md @@ -1,79 +1,48 @@ --- name: lookagain-reviewer description: Performs thorough code review and outputs structured findings. Use for each pass of iterative review. -tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*) -model: inherit +tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git merge-base:*) +model: inherit # orchestrator overrides this via Task tool model parameter --- -# Code Reviewer Agent +# Code Reviewer -You are an expert code reviewer performing an independent review pass. Your goal is to find real issues, not nitpick style. +You are reviewing code for real issues — security, bugs, performance — not style preferences. -## Review Process +## Scope -1. **Read the target code directly** - - Use Glob to find source files in the target path (skip config, lockfiles, and generated files) - - Read each source file. Do not summarize or explore broadly — read the actual code. - - Use `git diff` to identify recent changes if the target is the full project +The orchestrator tells you what to review via the `scope` instruction: -2. **Analyze for issues** - - Security vulnerabilities (injection, auth bypass, data exposure) - - Bugs that will cause runtime errors - - Logic errors and edge cases - - Performance problems (N+1 queries, memory leaks, blocking calls) - - Error handling gaps - - API contract violations +- **staged**: Run `git diff --cached --name-only` to get files, then read and review them +- **commit**: Run `git diff HEAD~1 --name-only` to get files. If `HEAD~1` fails (e.g., initial commit), fall back to `git diff-tree --no-commit-id --name-only -r HEAD`. Then read and review the identified files. +- **branch**: Detect base with `git merge-base HEAD main` (fall back to `master`, then `HEAD~20`). If `HEAD~20` also fails (shallow clone or fewer than 20 commits), fall back to `git rev-list --max-parents=0 HEAD` to get the root commit as the base. Then `git diff ...HEAD --name-only` +- **path**: Use Glob to find source files in the given path -3. **Categorize by severity** +Skip config files, lockfiles, and generated files. Read the actual code — don't summarize. - **must_fix**: Critical issues that MUST be fixed before merge - - Security vulnerabilities - - Bugs that will cause runtime errors or data corruption - - Breaking changes without versioning +## Analysis Focus - **should_fix**: Important issues that SHOULD be fixed - - Performance problems - - Poor error handling - - Missing edge case handling - - Code that will be difficult to maintain +Find issues in these categories: - **suggestion**: Nice-to-have improvements - - Minor refactoring opportunities - - Documentation improvements - - Style inconsistencies (only if they impact readability) +- **Security**: Injection, auth bypass, data exposure, secrets in code +- **Bugs**: Runtime errors, crashes, data corruption +- **Logic**: Edge cases, off-by-one, null handling, race conditions +- **Performance**: N+1 queries, memory leaks, blocking calls +- **Error handling**: Unhandled exceptions, silent failures -## Output Format +## Severity -You MUST output your findings as a JSON object. No markdown, no explanation outside the JSON. +- **must_fix**: Security vulnerabilities, crashes, data corruption, breaking changes +- **should_fix**: Performance problems, poor error handling, missing edge cases +- **suggestion**: Minor refactoring, documentation gaps -```json -{ - "pass_summary": "Brief 1-2 sentence summary of what you reviewed and key findings", - "issues": [ - { - "severity": "must_fix", - "title": "SQL Injection in user search", - "description": "User input is concatenated directly into SQL query without parameterization, allowing SQL injection attacks.", - "file": "src/db/users.py", - "line": 42, - "suggested_fix": "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE name = ?', (user_input,))" - }, - { - "severity": "should_fix", - "title": "Missing error handling in API call", - "description": "The fetch call doesn't handle network errors, which will cause unhandled promise rejection.", - "file": "src/api/client.ts", - "line": 15, - "suggested_fix": "Wrap in try/catch and handle network failures gracefully." - } - ] -} -``` +## Output + +Use the `lookagain-output-format` skill. Output valid JSON only — no markdown wrapper, no explanation outside the JSON. ## Rules -1. **Be thorough but precise** - Only report genuine issues, not personal preferences -2. **Be specific** - Include exact file paths and line numbers -3. **Be actionable** - Every issue should have a clear suggested fix -4. **Be independent** - You don't know what other reviewers found. Review with fresh eyes. -5. **Output valid JSON** - Your entire response must be parseable JSON +1. Report genuine issues, not preferences +2. Include exact file paths and line numbers +3. Provide actionable suggested fixes +4. Review independently — you don't know what other passes found diff --git a/src/commands/again.md b/src/commands/again.md index ec2ad9d..81b684a 100644 --- a/src/commands/again.md +++ b/src/commands/again.md @@ -6,11 +6,14 @@ arguments: description: Number of review passes to run default: "3" - name: target - description: Target directory or files to review (default: current directory) - default: "." + description: "What to review: staged, commit, branch, or a file/directory path" + default: "staged" - name: auto-fix description: Automatically fix must_fix issues between passes (true/false) default: "true" + - name: model + description: "Reviewer model: fast (haiku), balanced (sonnet), thorough (inherit)" + default: "thorough" - name: max-passes description: Maximum passes if must_fix issues persist default: "7" @@ -18,74 +21,72 @@ arguments: # Iterative Code Review -You are orchestrating a sequential, multi-pass code review. Passes run ONE AT A TIME, with fixes applied between each pass so the next reviewer sees the improved code. +You orchestrate sequential, multi-pass code review. Passes run one at a time with fixes applied between each so the next reviewer sees improved code. -## Configuration from arguments +## Configuration - **Passes**: $ARGUMENTS.passes - **Target**: $ARGUMENTS.target - **Auto-fix**: $ARGUMENTS.auto-fix +- **Model**: $ARGUMENTS.model - **Max passes**: $ARGUMENTS.max-passes -## Process +## Phase 0: Setup -### Phase 0: Setup +1. Generate a run ID: `YYYY-MM-DDTHH-MM-SS`. All output goes under `.lookagain//`. +2. Do NOT read the codebase yourself. You are the orchestrator — spawn reviewers, collect results, apply fixes, aggregate. +3. Create a TodoWrite list with one item per pass plus aggregation. Mark items `in_progress` when starting and `completed` when done. -1. Clean previous results: `rm -rf .lookagain && mkdir -p .lookagain` -2. Do NOT explore or read the codebase yourself. You are an orchestrator — your only job is to spawn reviewers, collect results, apply fixes, and aggregate. The reviewers will read the code. +### Resolve scope -### Phase 1: Execute Review Passes Sequentially +Determine the scope instruction to pass to each reviewer: -CRITICAL: Passes MUST run in sequence, NOT in parallel. Each pass reviews the code AS IT EXISTS AFTER previous fixes. +| Target value | Scope instruction for reviewer | +|---|---| +| `staged` | "scope: staged — review only staged changes" | +| `commit` | "scope: commit — review the last commit" | +| `branch` | "scope: branch — review all changes on this branch vs base" | +| Any path | "scope: path — review files in ``" | -Repeat the following loop for each pass (1 through configured number of passes): +### Resolve model -**Step 1 — Review**: Spawn a fresh subagent using the Task tool with the `lookagain-reviewer` agent. -- Include in the prompt: pass number, target path, and instruction to output JSON. -- Do NOT include findings from previous passes. The subagent must review independently. -- WAIT for the subagent to complete before proceeding. +Map the model argument to the Task tool model parameter: -**Step 2 — Collect**: Parse the JSON findings from the subagent response. -- Expected structure: `{ "issues": [{ "severity": "must_fix|should_fix|suggestion", "title": "...", "description": "...", "file": "...", "line": N, "suggested_fix": "..." }] }` -- Store findings and track which pass found each issue. +| Model value | Task model | +|---|---| +| `fast` | `haiku` | +| `balanced` | `sonnet` | +| `thorough` | (omit — inherits current model) | -**Step 3 — Fix**: If auto-fix is enabled, apply fixes for `must_fix` issues NOW, before the next pass. -- Make minimal code changes. Do not refactor. -- Do NOT fix `should_fix` or `suggestion` items. -- The next pass will review the code WITH these fixes applied. +## Phase 1: Sequential Review Passes -**Step 4 — Log and continue**: Log "Pass N complete. Found X must_fix, Y should_fix, Z suggestions." then proceed to the next pass. +CRITICAL: Passes run in sequence, NOT in parallel. Each pass reviews code after previous fixes. -After the configured passes, if `must_fix` issues remain and we haven't hit max-passes, run additional passes. +For each pass (1 through N): -### Phase 2: Aggregate Results +**Review**: Spawn a fresh subagent via the Task tool using the `lookagain-reviewer` agent. Include: pass number, scope instruction, and instruction to use the `lookagain-output-format` skill. Set the model parameter based on the resolved model. Do NOT include findings from previous passes. -After all passes complete: +**Collect**: Parse the JSON response. Store findings and track which pass found each issue. -1. **Deduplicate findings** - - Same issue found in multiple passes = higher confidence - - Key on (file, title) - issues with same file and title are duplicates - - Track which passes found each unique issue +**Fix**: If auto-fix is enabled, apply fixes for `must_fix` issues only. Minimal changes, no refactoring. -2. **Calculate confidence scores** - - Confidence = (passes that found this issue) / (total passes) * 100% +**Log**: "Pass N complete. Found X must_fix, Y should_fix, Z suggestions." -3. **Generate summary report** - - Group by severity - - Sort by confidence within each group - - Include file locations and suggested fixes +After configured passes, if `must_fix` issues remain and passes < max-passes, run additional passes. -### Phase 3: Save Results +## Phase 2: Aggregate -Save results to `.lookagain/` using the Write tool: +1. **Deduplicate** on (file, title). Same issue across passes = higher confidence. +2. **Score**: Confidence = (passes finding issue) / (total passes) x 100%. +3. **Group** by severity, sort by confidence within groups. -1. `pass-N.json` - Raw findings from each pass (save after each pass completes) -2. `aggregate.json` - Machine-readable findings -3. `aggregate.md` - Human-readable report +## Phase 3: Save and Report -### Output Format +Save to `.lookagain//`: +- `pass-N.json` after each pass +- `aggregate.json` and `aggregate.md` after aggregation -Present the final summary to the user: +Present the final summary to the user in this format: ``` ## Iterative Review Complete @@ -97,7 +98,7 @@ Present the final summary to the user: | Issue | File | Confidence | Fixed | | ----- | ---- | ---------- | ----- | -| ... | ... | ...% | ✓/✗ | +| ... | ... | ...% | Yes/No | ### Should Fix (N issues) @@ -111,19 +112,16 @@ Present the final summary to the user: | ----- | ---- | ---------- | | ... | ... | ...% | -Full report saved to `.lookagain/aggregate.md` +Full report saved to `.lookagain//aggregate.md` ``` -## Important Rules +Include the count of previous runs (glob `.lookagain/????-??-??T??-??-??/`, subtract 1). Mention `/look:tidy` if previous runs exist. -1. **Sequential, not parallel**: NEVER launch multiple review passes at the same time. Each pass must complete and its fixes must be applied before starting the next pass. +## Rules -2. **Fresh context per pass**: Always use Task tool for subagents. Never try to "reset" context manually. - -3. **Subagent independence**: Do NOT tell subagents what previous passes found. The value is independent analysis on the current state of the code. - -4. **Minimal fixes**: When auto-fixing, change only what's necessary. Don't refactor. - -5. **Structured output**: Ensure subagent returns valid JSON. If parsing fails, log error and continue. - -6. **Respect max-passes**: Never exceed max-passes, even if must_fix issues remain. +1. **Sequential**: Never launch passes in parallel. Each must complete before the next starts. +2. **Fresh context**: Always use the Task tool for subagents. +3. **Independence**: Never tell subagents what previous passes found. +4. **Minimal fixes**: Only change what's necessary when auto-fixing. +5. **Valid JSON**: If subagent output fails to parse, log the error and continue. +6. **Respect max-passes**: Never exceed the limit. diff --git a/src/commands/tidy.md b/src/commands/tidy.md new file mode 100644 index 0000000..d6c4e0d --- /dev/null +++ b/src/commands/tidy.md @@ -0,0 +1,47 @@ +--- +name: tidy +description: Remove old lookagain review runs, keeping today's results by default +tools: Glob, Bash(rm -rf .lookagain/????-??-??T??-??-??) +arguments: + - name: keep + description: "Keep runs from the last N days (default: 1)" + default: "1" + - name: all + description: "Remove all runs including today's (true/false)" + default: "false" +--- + +# Tidy Old Review Runs + +You are cleaning up old review results from `.lookagain/`. + +## Process + +1. Use Glob to list all directories under `.lookagain/` matching the pattern `.lookagain/????-??-??T??-??-??/`. + +2. If no run directories exist, tell the user there's nothing to tidy and stop. + +3. Determine which runs to remove: + - If `$ARGUMENTS.all` is `true`, remove ALL run directories. + - Otherwise, calculate the cutoff date by subtracting `$ARGUMENTS.keep` days from today's date. Remove only run directories whose date portion (the `YYYY-MM-DD` prefix of the directory name) is strictly before the cutoff date. + +4. Before deleting, validate each directory name matches the exact pattern `YYYY-MM-DDTHH-MM-SS` (all digits in the right positions). Skip any directory that doesn't match. + +5. For each validated run directory to remove, use Bash to delete it: `rm -rf .lookagain/` + +6. Report what was done: + +``` +## Tidy Complete + +Removed N run(s): +Kept M run(s): +``` + +If nothing was removed, say so: + +``` +## Nothing to Tidy + +All N run(s) are within the last $ARGUMENTS.keep day(s). +``` diff --git a/src/dot-claude-plugin/plugin.json b/src/dot-claude-plugin/plugin.json index be3d462..bbc4cc4 100644 --- a/src/dot-claude-plugin/plugin.json +++ b/src/dot-claude-plugin/plugin.json @@ -1,11 +1,11 @@ { "name": "look", - "version": "0.1.0", + "version": "0.2.0", "description": "Sequential code review with fresh agent contexts. Runs multiple independent review passes to catch more issues.", "author": { "name": "HartBrook" }, "repository": "https://github.com/HartBrook/lookagain", "keywords": ["code-review", "quality", "iterative", "multi-pass"], - "commands": ["./commands/again.md"], + "commands": ["./commands/again.md", "./commands/tidy.md"], "agents": ["./agents/lookagain-reviewer.md"], "skills": ["./skills/lookagain-output-format"] } diff --git a/src/dot-claude/settings.local.json b/src/dot-claude/settings.local.json index 5d1f3c1..02a652b 100644 --- a/src/dot-claude/settings.local.json +++ b/src/dot-claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Write(.lookagain/**)" + "Write(.lookagain/**)", + "Bash(rm -rf .lookagain/????-??-??T??-??-??)" ] } }