diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 72e5974..b8a7ff0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,8 +8,8 @@ "name": "matrix", "source": "./", "description": "Claude on Rails Tooling System - Persistent memory for Claude Code", - "version": "2.0.1" + "version": "2.0.2" } ], - "version": "2.0.1" + "version": "2.0.2" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a70c265..e28f815 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "matrix", "description": "Claude on Rails Tooling System", - "version": "2.0.1", + "version": "2.0.2", "author": { "name": "Matrix Contributors" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index c01fe98..9751bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,91 @@ All notable changes to Claude Matrix are documented here. +## [2.0.2] - 2025-01-13 + +### Added + +#### Subagent Hooks +- **New `SubagentStart` Hook** - Inject Matrix guidance when subagents spawn + - Fires when Explore, Plan, or other subagents start + - Injects guidance to prefer Matrix index tools over Grep for code search + - Injects guidance to prefer Context7 over WebSearch for library docs + - Respects `toolSearch.preferMatrixIndex` and `toolSearch.preferContext7` config + - Verbosity-aware output (full/compact/minimal) + - Agent-specific hints for explore/plan agents to use `matrix_recall` + +- **New `SubagentStop` Hook** - Track subagent completion + - Fires when Explore, Plan, or other subagents complete + - Logs completion in verbose mode + +#### Token Optimization +- **Reduced MCP Tool Token Usage** - ~10-12% reduction in Matrix tool definitions + - Shortened parameter descriptions across all schemas + - Removed redundant "Optional:", "default:" phrases + - Optimized tool description verbosity + +#### Index Tools Accessibility +- **Index Tools Always Available** - Can now use from any directory + - Changed visibility from `'indexable'` → `'always'` for query tools + - Pass `repoPath` parameter to query any indexed repository + - Tools: `matrix_find_definition`, `matrix_find_callers`, `matrix_list_exports`, `matrix_search_symbols`, `matrix_get_imports` + +#### Auto-Install Features +- **File Suggestion Script** - Auto-installs `~/.claude/file-suggestion.sh` + - Uses ripgrep + fzf for fast fuzzy file matching + - Follows symlinks and respects .gitignore + - Auto-merges `fileSuggestion` config into `~/.claude/settings.json` + +#### Model Delegation Config +- **New `delegation` Config Section** - Control sub-agent model selection + - `enabled`: Toggle delegation (default: true) + - `model`: `'haiku'` or `'sonnet'` (default: `'haiku'`) + - MCP instructions updated to tell Claude which model to use for read-only tools + +#### Config Auto-Upgrade +- **Session Start Config Migration** - Automatically adds missing config sections + - Detects missing: `memoryInjection`, `permissions`, `userRules`, `gitCommitReview`, `delegation` + - Preserves existing user settings while adding new defaults + - No manual `/matrix:doctor` needed after upgrades + +### Changed + +#### Code Review Refactored +- **Simplified Review Modes** - Two modes instead of three depths + - `default`: Comprehensive 5-phase review with full index utilization + - `lazy`: Quick single-pass review, no index queries +- **New Config Structure** - `hooks.gitCommitReview` + - `suggestOnCommit`: Suggest review before commits (default: true) + - `defaultMode`: `'default'` or `'lazy'` (default: `'default'`) + - `autoRun`: Never auto-runs, always suggests (default: false) +- **BREAKING**: Removed `depth` setting (`quick`/`standard`/`thorough`) + +#### Memory Injection Config +- **Proper Config Usage** - `hooks.promptAnalysis.memoryInjection` now actually used + - `enabled`: Toggle memory injection (default: true) + - `maxSolutions`: Max solutions to inject (default: 3) + - `maxFailures`: Max failures to inject (default: 2) + - `minScore`: Minimum similarity score (default: 0.35) + +#### Wildcard Permission Patterns +- **Simplified PermissionRequest matchers** - Leverages Claude Code 2.1.0+ wildcards + - `mcp__plugin_matrix_matrix__*` replaces 11 individual tool matchers + - `mcp__plugin_matrix_context7__*` replaces 2 individual tool matchers + - Auto-includes new tools added to MCP servers + +#### Increased Hook Timeouts +- **Leverage Claude Code 2.1.3's 10-minute limit** for complex analysis + - `UserPromptSubmit`: 60s → **600s** (deep-research, review commands) + - `PreCompact`: 30s → **600s** (session analysis before compaction) + - `PreToolUse:Bash`: 30s → 60s (package auditing) + - `Stop`: 30s → 60s (session summary) + +### Fixed + +- Removed unused imports in `session-start.ts` (oxlint warnings) + +--- + ## [2.0.1] - 2025-01-09 ### Added diff --git a/README.md b/README.md index 71aec40..be8b6d6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Persistent Memory & Tooling for Claude Code** -[![Version](https://img.shields.io/badge/v2.0.0-blue.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/v2.0.2-blue.svg)](CHANGELOG.md) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![LLM Reference](https://img.shields.io/badge/LLM-reference-purple.svg)](docs/reference-for-llms.md) @@ -52,18 +52,23 @@ Verify with `/matrix:doctor` ## What's New in v2.0 -### Core Improvements +### v2.0.2 +- **Subagent Hooks** — `SubagentStart` injects Matrix guidance to Explore/Plan agents +- **Wildcard Warnings** — Glob patterns in warning rules (`src/legacy/**`) +- **Hook Timeouts** — Configurable timeout (default 30s, max 120s) +- **Index Tools Anywhere** — Query any indexed repo via `repoPath` parameter +- **Token Optimization** — ~10-12% reduction in MCP tool definitions + +### v2.0.0 - **Hook Verbosity** — `compact` mode cuts token overhead by 80% - **Unified Warn API** — Single `matrix_warn` tool with `action` parameter - -### New Capabilities - **Skill Factory** — Promote high-value solutions to Claude Code Skills - **Blast Radius** — `matrix_find_callers` shows impact before changes - **Code Review** — 5-phase review pipeline via `/matrix:review` - **Deep Research** — Multi-source aggregation via `/matrix:deep-research` - **User Rules** — Custom pattern matching (block, warn, allow) -### Breaking Changes +### Breaking Changes (v2.0) - Removed: `/matrix:verify`, `/matrix:stats`, `/matrix:search` - Changed: Four warn tools → single `matrix_warn` diff --git a/commands/review.md b/commands/review.md index 5dda9bb..b647b60 100644 --- a/commands/review.md +++ b/commands/review.md @@ -4,22 +4,35 @@ description: Conduct a multi-phase code review with blast radius analysis # Matrix Code Review -Perform a comprehensive, context-aware code review using Matrix's 5-phase review pipeline. +Perform a comprehensive, context-aware code review using Matrix's review pipeline with full index integration. + +> **Tip:** For best results, run `/matrix:review` in a **fresh session**. A new session has no prior context about the code, which provides an unbiased perspective—similar to how a human reviewer would approach the code for the first time. ## Usage Parse arguments: `$ARGUMENTS` -**Expected format:** ` [depth]` +**Expected format:** ` [mode]` - **target**: File path, PR number, or "staged" for staged changes -- **depth** (optional): `quick` | `standard` | `thorough` (default: `standard`) +- **mode** (optional): `default` | `lazy` (default: from config or `default`) + +## Modes -## Depth Levels +### Default Mode (Comprehensive) +Full 5-phase review pipeline with maximum index utilization: +- Blast radius analysis via `matrix_find_callers` +- Symbol lookup via `matrix_find_definition` and `matrix_search_symbols` +- Memory recall via `matrix_recall` for relevant past solutions +- Deep security and edge case analysis +- ~10+ comments, thorough coverage -- **quick**: Single-pass review, main issues only (~2-3 comments) -- **standard**: Full pipeline, balanced coverage (~5-10 comments) -- **thorough**: Deep analysis, edge cases, security review (~10+ comments) +### Lazy Mode (Quick) +Single-pass review for fast feedback: +- Direct code inspection only +- No index queries (faster) +- Main issues only +- ~2-3 comments ## 5-Phase Review Pipeline @@ -200,9 +213,11 @@ Generate final review output in Greptile-style format: ## Examples ``` -/matrix:review src/utils/auth.ts standard -/matrix:review staged thorough -/matrix:review 123 quick +/matrix:review src/utils/auth.ts # Default mode (comprehensive) +/matrix:review staged # Review staged changes +/matrix:review staged lazy # Quick review of staged changes +/matrix:review 123 # Review PR #123 +/matrix:review 123 lazy # Quick review of PR #123 ``` ## Output Location diff --git a/hooks/hooks.json b/hooks/hooks.json index c53c90a..2391054 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -17,7 +17,7 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/run-hooks.sh user-prompt-submit", - "timeout": 60 + "timeout": 600 } ] } @@ -34,7 +34,7 @@ ] }, { - "matcher": "mcp__plugin_matrix_matrix__matrix_recall|mcp__plugin_matrix_matrix__matrix_status|mcp__plugin_matrix_matrix__matrix_warn|mcp__plugin_matrix_matrix__matrix_find_definition|mcp__plugin_matrix_matrix__matrix_search_symbols|mcp__plugin_matrix_matrix__matrix_list_exports|mcp__plugin_matrix_matrix__matrix_get_imports|mcp__plugin_matrix_matrix__matrix_index_status|mcp__plugin_matrix_matrix__matrix_reindex|mcp__plugin_matrix_matrix__matrix_repomix|mcp__plugin_matrix_matrix__matrix_prompt", + "matcher": "mcp__plugin_matrix_matrix__*", "hooks": [ { "type": "command", @@ -44,7 +44,7 @@ ] }, { - "matcher": "mcp__plugin_matrix_context7__resolve-library-id|mcp__plugin_matrix_context7__query-docs", + "matcher": "mcp__plugin_matrix_context7__*", "hooks": [ { "type": "command", @@ -71,7 +71,7 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/run-hooks.sh pre-tool-bash", - "timeout": 30 + "timeout": 60 } ] }, @@ -124,7 +124,7 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/run-hooks.sh pre-compact", - "timeout": 30 + "timeout": 600 } ] } @@ -135,7 +135,29 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/run-hooks.sh stop-session", - "timeout": 30 + "timeout": 60 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/run-hooks.sh subagent-start", + "timeout": 10 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/run-hooks.sh subagent-stop", + "timeout": 10 } ] } diff --git a/package.json b/package.json index bc2fb05..8e9c0fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-matrix", - "version": "2.0.1", + "version": "2.0.2", "description": "Claude on Rails Tooling System", "type": "module", "main": "src/index.ts", diff --git a/src/__tests__/registry.test.ts b/src/__tests__/registry.test.ts index dc08a22..bf4e5d9 100644 --- a/src/__tests__/registry.test.ts +++ b/src/__tests__/registry.test.ts @@ -105,9 +105,9 @@ describe('Tool Registry', () => { expect(toolNames).toContain('matrix_warn'); expect(toolNames).toContain('matrix_doctor'); - // Index tools should be hidden (not indexable) - expect(toolNames).not.toContain('matrix_find_definition'); - expect(toolNames).not.toContain('matrix_search_symbols'); + // Index tools should also be visible (can query any repo via repoPath) + expect(toolNames).toContain('matrix_find_definition'); + expect(toolNames).toContain('matrix_search_symbols'); }); it('should show index tools for indexable projects', async () => { diff --git a/src/config/index.ts b/src/config/index.ts index 36222a5..87f824e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -101,12 +101,15 @@ export interface PromptAnalysisConfig { } // ═══════════════════════════════════════════════════════════════ -// Pre-Commit Review Config +// Code Review Config // ═══════════════════════════════════════════════════════════════ export interface GitCommitReviewConfig { - enabled: boolean; - /** Review depth: 'quick' | 'standard' | 'thorough' */ - depth: 'quick' | 'standard' | 'thorough'; + /** Suggest review before git commits */ + suggestOnCommit: boolean; + /** Default review mode: 'default' (comprehensive) | 'lazy' (quick) */ + defaultMode: 'default' | 'lazy'; + /** Auto-run review (NOT recommended - prefer suggestion) */ + autoRun: boolean; } // ═══════════════════════════════════════════════════════════════ @@ -204,6 +207,13 @@ export interface MatrixConfig { /** Log when tools are shown/hidden due to project context */ verbose: boolean; }; + /** Model delegation settings for read-only tools */ + delegation: { + /** Enable tool delegation to cheaper models */ + enabled: boolean; + /** Model for delegable tools: 'haiku' (cheaper) or 'sonnet' (more capable) */ + model: 'haiku' | 'sonnet'; + }; } function getDownloadsDirectory(): string { @@ -327,11 +337,12 @@ export const DEFAULT_CONFIG: MatrixConfig = { }, }, - // ─── Pre-Commit Review (PreToolUse:Bash hook) ─── - // Triggers Matrix review before git/jj commits + // ─── Code Review Config ─── + // Controls /matrix:review behavior and commit suggestions gitCommitReview: { - enabled: true, - depth: 'standard' as const, + suggestOnCommit: true, // Suggest review before git commits + defaultMode: 'default' as const, // 'default' (comprehensive) or 'lazy' (quick) + autoRun: false, // Never auto-run, always suggest }, // ─── User Rules (v2.0) ─── @@ -380,6 +391,10 @@ export const DEFAULT_CONFIG: MatrixConfig = { preferContext7: true, verbose: false, }, + delegation: { + enabled: true, + model: 'haiku' as const, // Use haiku for cheaper read-only operations + }, }; let cachedConfig: MatrixConfig | null = null; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 2d2f434..2cbcdb4 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -43,6 +43,26 @@ export interface StopInput extends HookInput { stop_hook_active: boolean; } +/** + * SubagentStart hook input (Claude Code 2.0.43+) + * Fires when a subagent (Explore, Plan, etc.) starts + */ +export interface SubagentStartInput extends HookInput { + agent_id: string; + agent_type: string; + hook_event_name: 'SubagentStart'; +} + +/** + * SubagentStop hook input (Claude Code 2.0.42+) + * Fires when a subagent completes + */ +export interface SubagentStopInput extends HookInput { + agent_id: string; + agent_transcript_path: string; + stop_hook_active?: boolean; +} + /** * PermissionRequest decision structure */ diff --git a/src/hooks/pre-tool-bash.ts b/src/hooks/pre-tool-bash.ts index 1bd77ee..bef22d1 100644 --- a/src/hooks/pre-tool-bash.ts +++ b/src/hooks/pre-tool-bash.ts @@ -298,15 +298,16 @@ export async function run() { const gitReviewConfig = config.hooks.gitCommitReview; const commitCheck = isCommitCommand(command); - if (gitReviewConfig?.enabled && commitCheck.isCommit) { - const depth = gitReviewConfig.depth ?? 'standard'; + if (gitReviewConfig?.suggestOnCommit && commitCheck.isCommit) { + const mode = gitReviewConfig.defaultMode ?? 'default'; const vcsName = commitCheck.vcs === 'jj' ? 'Jujutsu' : 'Git'; + const modeArg = mode === 'lazy' ? ' lazy' : ''; // Suggest review before commit (non-blocking) const output: HookOutput = { hookSpecificOutput: { hookEventName: 'PreToolUse', - additionalContext: `[Matrix] ${vcsName} commit detected. Consider running a code review first:\n\n/matrix:review staged ${depth}\n\nThis helps catch issues before they're committed.`, + additionalContext: `[Matrix] ${vcsName} commit detected. Consider running a code review first:\n\n\`/matrix:review staged${modeArg}\`\n\nThis helps catch issues before they're committed.`, }, }; diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index 30ab186..f735b00 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -20,10 +20,94 @@ import { homedir } from 'os'; import { Database } from 'bun:sqlite'; import { createHash } from 'crypto'; import { spawnSync } from 'child_process'; -import { getConfig } from '../config/index.js'; +import { getConfig, saveConfig, clearCache } from '../config/index.js'; import { runMigrations } from '../db/migrate.js'; const CURRENT_VERSION = '1.0.3'; +const CLAUDE_DIR = join(homedir(), '.claude'); +const FILE_SUGGESTION_DEST = join(CLAUDE_DIR, 'file-suggestion.sh'); +const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json'); + +// Embedded file-suggestion.sh content +const FILE_SUGGESTION_SCRIPT = `#!/bin/bash +# Custom file suggestion script for Claude Code (installed by Matrix) +# Uses rg + fzf for fuzzy matching and symlink support +# Prerequisites: brew install ripgrep jq fzf + +QUERY=$(jq -r '.query // ""') +PROJECT_DIR="\${CLAUDE_PROJECT_DIR:-.}" +cd "$PROJECT_DIR" || exit 1 +rg --files --follow --hidden . 2>/dev/null | sort -u | fzf --filter "$QUERY" | head -15 +`; + +/** + * Install file-suggestion.sh and update settings.json + * Returns true if any changes were made + */ +function installFileSuggestion(): boolean { + let changed = false; + + try { + // Install the script if not present + if (!existsSync(FILE_SUGGESTION_DEST)) { + writeFileSync(FILE_SUGGESTION_DEST, FILE_SUGGESTION_SCRIPT, { mode: 0o755 }); + changed = true; + } + + // Update settings.json to use it + let settings: Record = {}; + if (existsSync(SETTINGS_PATH)) { + try { + settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); + } catch { + // Invalid JSON, start fresh + settings = {}; + } + } + + // Add fileSuggestion if not configured (respects explicit null/false) + if (!('fileSuggestion' in settings)) { + settings.fileSuggestion = { + type: 'command', + command: '~/.claude/file-suggestion.sh', + }; + writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2)); + changed = true; + } + + return changed; + } catch { + return false; + } +} + +/** + * Check for missing config sections and save if needed + * This ensures config file is updated with new sections on upgrade + */ +function ensureConfigComplete(): void { + try { + clearCache(); // Get fresh config from disk + const config = getConfig(); + + // Check for v2.0+ required sections + const missingSections: string[] = []; + if (!config.hooks?.promptAnalysis?.memoryInjection) missingSections.push('memoryInjection'); + if (!config.hooks?.permissions) missingSections.push('permissions'); + if (!config.hooks?.userRules) missingSections.push('userRules'); + if (!config.hooks?.gitCommitReview) missingSections.push('gitCommitReview'); + if (!config.indexing) missingSections.push('indexing'); + if (!config.toolSearch) missingSections.push('toolSearch'); + if (!config.delegation) missingSections.push('delegation'); + + if (missingSections.length > 0) { + // getConfig() already merged with defaults, just save it + saveConfig(config); + } + } catch { + // Config issues will be caught by doctor + } +} const MATRIX_DIR = join(homedir(), '.claude', 'matrix'); const MARKER_FILE = join(MATRIX_DIR, '.initialized'); const DB_PATH = join(MATRIX_DIR, 'matrix.db'); @@ -309,6 +393,12 @@ export async function run() { writeFileSync(MARKER_FILE, JSON.stringify(state, null, 2)); } + // Ensure config has all v2.0+ sections (auto-upgrades old configs) + ensureConfigComplete(); + + // Install file-suggestion.sh and update settings.json (silent) + installFileSuggestion(); + // Run indexer for supported projects (if enabled) const config = getConfig(); if (config.indexing.enabled) { diff --git a/src/hooks/subagent-start.ts b/src/hooks/subagent-start.ts new file mode 100644 index 0000000..5b20423 --- /dev/null +++ b/src/hooks/subagent-start.ts @@ -0,0 +1,142 @@ +#!/usr/bin/env bun +/** + * SubagentStart Hook + * + * Runs when a subagent (Explore, Plan, etc.) starts. + * Injects Matrix-specific guidance: + * - Prefer Matrix index tools over Grep for code search + * - Prefer Context7 over WebSearch for library docs + * + * Exit codes: + * 0 = Success (output used by Claude Code) + * 1 = Non-blocking error (stderr shown to user) + */ + +import { + readStdin, + outputJson, + hooksEnabled, + type SubagentStartInput, + type HookOutput, +} from './index.js'; +import { getConfig } from '../config/index.js'; +import { getVerbosity } from './format-helpers.js'; + +/** + * Build guidance context for subagents based on config + */ +function buildSubagentGuidance(agentType: string): string[] { + const config = getConfig(); + const toolSearch = config.toolSearch; + const verbosity = getVerbosity(); + + const guidance: string[] = []; + + // Matrix Index preference + if (toolSearch.preferMatrixIndex) { + if (verbosity === 'compact' || verbosity === 'minimal') { + guidance.push( + '[Matrix] Prefer matrix_find_definition, matrix_search_symbols over Grep for code search' + ); + } else { + guidance.push( + '[Matrix Guidance] For code navigation and symbol search, prefer these Matrix tools:', + ' - matrix_find_definition: Find where a symbol is defined', + ' - matrix_search_symbols: Search for symbols by name pattern', + ' - matrix_list_exports: List exported symbols from a file', + ' - matrix_get_imports: Get imports for a file', + 'These are faster and more accurate than Grep for code structure queries.' + ); + } + } + + // Context7 preference + if (toolSearch.preferContext7) { + if (verbosity === 'compact' || verbosity === 'minimal') { + guidance.push( + '[Matrix] Prefer Context7 (resolve-library-id + query-docs) over WebSearch for library docs' + ); + } else { + guidance.push( + '[Matrix Guidance] For library documentation lookup, prefer Context7 tools:', + ' 1. resolve-library-id: Get the Context7 library ID for a package', + ' 2. query-docs: Query documentation with the resolved library ID', + 'Context7 provides accurate, up-to-date documentation without web search noise.' + ); + } + } + + // Agent-specific guidance for explore/plan agents + const agentTypeLower = (agentType || '').toLowerCase(); + if (agentTypeLower === 'explore' || agentTypeLower === 'plan') { + if (verbosity === 'full') { + guidance.push( + `[Matrix Guidance for ${agentType} agent]`, + 'Use matrix_recall to check for existing solutions before implementing new ones.', + 'Use matrix_index_status to check if code index is available for this repository.' + ); + } else if (verbosity === 'compact') { + guidance.push('[Matrix] Check matrix_recall for existing solutions'); + } + // minimal: skip agent-specific guidance + } + + return guidance; +} + +export async function run() { + try { + // Check if hooks are enabled + if (!hooksEnabled()) { + process.exit(0); + } + + // Read input from stdin + const input = await readStdin(); + + // Get config + const config = getConfig(); + const toolSearch = config.toolSearch; + + // Skip if tool search preferences are all disabled + if (!toolSearch.preferMatrixIndex && !toolSearch.preferContext7) { + process.exit(0); + } + + // Build guidance + const guidance = buildSubagentGuidance(input.agent_type); + + if (guidance.length === 0) { + process.exit(0); + } + + // Format guidance as context + const additionalContext = guidance.join('\n'); + + // Build output + const output: HookOutput = { + hookSpecificOutput: { + additionalContext, + }, + }; + + // Optional: show terminal message in verbose mode + if (toolSearch.verbose) { + const verbosity = getVerbosity(); + if (verbosity !== 'minimal') { + output.systemMessage = `[Matrix] Injected guidance for ${input.agent_type || 'unknown'} subagent`; + } + } + + outputJson(output); + process.exit(0); + } catch (err) { + // Log error but don't block subagent + console.error( + `[Matrix] SubagentStart hook error: ${err instanceof Error ? err.message : err}` + ); + process.exit(1); + } +} + +if (import.meta.main) run(); diff --git a/src/hooks/subagent-stop.ts b/src/hooks/subagent-stop.ts new file mode 100644 index 0000000..1e0130a --- /dev/null +++ b/src/hooks/subagent-stop.ts @@ -0,0 +1,56 @@ +#!/usr/bin/env bun +/** + * SubagentStop Hook + * + * Runs when a subagent (Explore, Plan, etc.) completes. + * Logs subagent completion for visibility. + * + * Exit codes: + * 0 = Success + * 1 = Non-blocking error (stderr shown to user) + */ + +import { + readStdin, + outputJson, + log, + hooksEnabled, + type SubagentStopInput, + type HookOutput, +} from './index.js'; +import { getConfig } from '../config/index.js'; +import { getVerbosity } from './format-helpers.js'; + +export async function run() { + try { + // Check if hooks are enabled + if (!hooksEnabled()) { + process.exit(0); + } + + // Read input from stdin + const input = await readStdin(); + + // Get verbosity + const verbosity = getVerbosity(); + const config = getConfig(); + + // Log completion in verbose mode + if (config.toolSearch.verbose && verbosity !== 'minimal') { + log(`[Matrix] Subagent ${input.agent_id || 'unknown'} completed`); + } + + // Output empty - no context injection needed for stop + const output: HookOutput = {}; + outputJson(output); + process.exit(0); + } catch (err) { + // Log error but don't block + console.error( + `[Matrix] SubagentStop hook error: ${err instanceof Error ? err.message : err}` + ); + process.exit(1); + } +} + +if (import.meta.main) run(); diff --git a/src/hooks/unified-entry.ts b/src/hooks/unified-entry.ts index 3c036b1..96ffd31 100644 --- a/src/hooks/unified-entry.ts +++ b/src/hooks/unified-entry.ts @@ -10,6 +10,8 @@ import { run as preToolEdit } from './pre-tool-edit.js'; import { run as preToolWeb } from './pre-tool-web.js'; import { run as preCompact } from './pre-compact.js'; import { run as stopSession } from './stop-session.js'; +import { run as subagentStart } from './subagent-start.js'; +import { run as subagentStop } from './subagent-stop.js'; const hooks: Record Promise> = { 'session-start': sessionStart, @@ -23,6 +25,8 @@ const hooks: Record Promise> = { 'pre-tool-web': preToolWeb, 'pre-compact': preCompact, 'stop-session': stopSession, + 'subagent-start': subagentStart, + 'subagent-stop': subagentStop, }; const hookType = process.argv[2]; diff --git a/src/hooks/user-prompt-submit.ts b/src/hooks/user-prompt-submit.ts index 88c50ce..da42d66 100644 --- a/src/hooks/user-prompt-submit.ts +++ b/src/hooks/user-prompt-submit.ts @@ -291,16 +291,45 @@ export async function run() { } // ============================================ - // STEP 4: Search Matrix memory + // STEP 4: Search Matrix memory (if enabled) // ============================================ + const memoryConfig = config.promptAnalysis?.memoryInjection ?? { + enabled: true, + maxSolutions: 3, + maxFailures: 2, + minScore: 0.35, + }; + + // Skip memory injection if disabled in config + if (!memoryConfig.enabled) { + // Still inject prompt agent context and code index if available + const verbosity = getVerbosity(); + const noMemoryParts: (string | null)[] = []; + + if (promptAnalysis.contextInjected.length > 0) { + if (verbosity === 'full') { + noMemoryParts.push(`[Prompt Context]\n${promptAnalysis.contextInjected.join('\n')}`); + } else { + noMemoryParts.push(promptAnalysis.contextInjected.join('\n')); + } + } + noMemoryParts.push(codeIndexContext); + + const assembled = assembleContext(noMemoryParts, verbosity); + if (assembled) { + outputText(assembled); + } + process.exit(0); + } + const recallResult = await matrixRecall({ - query: input.prompt.slice(0, 500), // Shorter query = less compute - limit: 3, - minScore: 0.55, // Higher threshold = fewer irrelevant injections + query: input.prompt.slice(0, 500), + limit: memoryConfig.maxSolutions, + minScore: memoryConfig.minScore, }); // Also search for related failures - const failures = await searchFailures(input.prompt.slice(0, 500), 2); + const failures = await searchFailures(input.prompt.slice(0, 500), memoryConfig.maxFailures); // Format context const context = formatContext( diff --git a/src/index.ts b/src/index.ts index 6e03944..a039a1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,13 +59,18 @@ ${subagentInstructions.join('\n')}`); } } - // Cost Optimization (always included) - sections.push(`## Cost Optimization -Delegate read-only tools to Haiku sub-agents via Task(model="haiku"): + // Cost Optimization (controlled by delegation config) + if (config.delegation?.enabled) { + const model = config.delegation.model || 'haiku'; + sections.push(`## Cost Optimization +Delegate read-only tools to ${model === 'haiku' ? 'Haiku' : 'Sonnet'} sub-agents via Task(model="${model}"): - matrix_recall, matrix_reward, matrix_status - matrix_warn (all actions: check, add, remove, list) - matrix_find_definition, matrix_search_symbols, matrix_list_exports, matrix_get_imports -- matrix_index_status, matrix_reindex`); +- matrix_index_status, matrix_reindex + +IMPORTANT: When using these tools, spawn a Task agent with model="${model}" to reduce costs.`); + } // When to Use Matrix Tools (always included) sections.push(`## When to Use Matrix Tools diff --git a/src/tools/doctor.ts b/src/tools/doctor.ts index c667113..e6747a5 100644 --- a/src/tools/doctor.ts +++ b/src/tools/doctor.ts @@ -138,6 +138,31 @@ function checkDatabase(): DiagnosticCheck { } } +/** + * Check config file for missing sections + * Returns list of missing config paths that need to be added + */ +function findMissingConfigSections(config: ReturnType): string[] { + const missing: string[] = []; + + // Check for required config sections (added in v2.0+) + if (!config.hooks) missing.push('hooks'); + if (!config.hooks?.promptAnalysis) missing.push('hooks.promptAnalysis'); + if (!config.hooks?.promptAnalysis?.memoryInjection) missing.push('hooks.promptAnalysis.memoryInjection'); + if (!config.hooks?.permissions) missing.push('hooks.permissions'); + if (!config.hooks?.preCompact) missing.push('hooks.preCompact'); + if (!config.hooks?.sensitiveFiles) missing.push('hooks.sensitiveFiles'); + if (!config.hooks?.stop) missing.push('hooks.stop'); + if (!config.hooks?.packageAuditor) missing.push('hooks.packageAuditor'); + if (!config.hooks?.cursedFiles) missing.push('hooks.cursedFiles'); + if (!config.hooks?.userRules) missing.push('hooks.userRules'); + if (!config.indexing) missing.push('indexing'); + if (!config.toolSearch) missing.push('toolSearch'); + if (!config.delegation) missing.push('delegation'); + + return missing; +} + /** * Check config file */ @@ -146,12 +171,13 @@ function checkConfig(): DiagnosticCheck { try { const config = getConfig(); + const missingSections = findMissingConfigSections(config); - if (!config.hooks) { + if (missingSections.length > 0) { return { name: 'Configuration', status: 'warn', - message: 'Config missing hooks section', + message: 'Missing sections: ' + missingSections.slice(0, 3).join(', ') + (missingSections.length > 3 ? ' (+' + (missingSections.length - 3) + ' more)' : ''), autoFixable: true, fixAction: 'Add missing sections (preserves user settings)', }; diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 67fcb13..001b83d 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -114,14 +114,14 @@ export const TOOLS: Tool[] = [ // ═══════════════════════════════════════════════════════════════ { name: 'matrix_prompt', - description: 'Analyze and optimize a prompt before execution. Detects ambiguity, infers context, and either returns an optimized prompt or asks clarification questions. Use for complex or ambiguous user requests.', + description: 'Analyze and optimize a prompt. Detects ambiguity, infers context, returns optimized prompt or asks clarification questions.', annotations: { readOnlyHint: true }, inputSchema: toInputSchema(PromptInputSchema), _meta: { category: 'utility' as ToolCategory, visibility: 'always' as VisibilityRule }, }, { name: 'matrix_repomix', - description: 'Pack external repositories for context. Two-phase flow: Phase 1 (no files) returns suggested files based on query. Phase 2 (with confirmedFiles) packs those files. Minimizes token consumption by letting you confirm before packing.', + description: 'Pack external repositories for context. Phase 1: suggest files. Phase 2 (with confirmedFiles): pack them.', annotations: { readOnlyHint: true, openWorldHint: true }, inputSchema: toInputSchema(RepomixInputSchema), _meta: { category: 'utility' as ToolCategory, visibility: 'always' as VisibilityRule }, @@ -146,49 +146,49 @@ export const TOOLS: Tool[] = [ }, { name: 'matrix_reindex', - description: 'Manually trigger repository reindexing. Use to refresh the code index after changes.', + description: 'Manually trigger repository reindexing.', annotations: { idempotentHint: true }, inputSchema: toInputSchema(ReindexInputSchema), _meta: { delegable: true, category: 'index-mgmt' as ToolCategory, visibility: 'indexable' as VisibilityRule }, }, // ═══════════════════════════════════════════════════════════════ - // Code Index Query Tools - Visible only if project is indexable + // Code Index Query Tools - Always visible, use repoPath for other repos // ═══════════════════════════════════════════════════════════════ { name: 'matrix_find_definition', - description: 'Find where a symbol (function, class, type, variable) is defined in the codebase.', + description: 'Find where a symbol is defined. Pass repoPath to query other repos.', annotations: { readOnlyHint: true }, inputSchema: toInputSchema(FindDefinitionInputSchema), - _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'indexable' as VisibilityRule }, + _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'always' as VisibilityRule }, }, { name: 'matrix_find_callers', - description: 'Find all files that import and use a symbol. Useful for blast radius calculation in code reviews.', + description: 'Find all files that import and use a symbol. Useful for blast radius analysis.', annotations: { readOnlyHint: true }, inputSchema: toInputSchema(FindCallersInputSchema), - _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'indexable' as VisibilityRule }, + _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'always' as VisibilityRule }, }, { name: 'matrix_list_exports', description: 'List exported symbols from a file or directory.', annotations: { readOnlyHint: true }, inputSchema: toInputSchema(ListExportsInputSchema), - _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'indexable' as VisibilityRule }, + _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'always' as VisibilityRule }, }, { name: 'matrix_search_symbols', - description: 'Search for symbols by partial name match. Use when you know part of a symbol name.', + description: 'Search for symbols by partial name match.', annotations: { readOnlyHint: true }, inputSchema: toInputSchema(SearchSymbolsInputSchema), - _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'indexable' as VisibilityRule }, + _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'always' as VisibilityRule }, }, { name: 'matrix_get_imports', description: 'Get all imports in a file.', annotations: { readOnlyHint: true }, inputSchema: toInputSchema(GetImportsInputSchema), - _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'indexable' as VisibilityRule }, + _meta: { delegable: true, category: 'index' as ToolCategory, visibility: 'always' as VisibilityRule }, }, // ═══════════════════════════════════════════════════════════════ @@ -196,14 +196,14 @@ export const TOOLS: Tool[] = [ // ═══════════════════════════════════════════════════════════════ { name: 'matrix_skill_candidates', - description: 'List Matrix solutions that are good candidates for promotion to Claude Code Skills. Returns solutions with high success rates and usage.', + description: 'List solutions good for promotion to Skills. Returns high success rate solutions.', annotations: { readOnlyHint: true }, inputSchema: toInputSchema(SkillCandidatesInputSchema), _meta: { delegable: true, category: 'utility' as ToolCategory, visibility: 'always' as VisibilityRule }, }, { name: 'matrix_link_skill', - description: 'Link a Matrix solution to a Claude Code Skill file. Marks the solution as promoted and prevents duplicate promotions.', + description: 'Link a solution to a Skill file. Marks as promoted.', annotations: { idempotentHint: true }, inputSchema: toInputSchema(LinkSkillInputSchema), _meta: { category: 'utility' as ToolCategory, visibility: 'always' as VisibilityRule }, diff --git a/src/tools/validation.ts b/src/tools/validation.ts index caabe1b..58b5abf 100644 --- a/src/tools/validation.ts +++ b/src/tools/validation.ts @@ -91,20 +91,12 @@ const PromptModeEnum = Type.Union([ // ============================================================================ export const RecallInputSchema = Type.Object({ - query: Type.String({ description: 'What problem are you trying to solve?' }), - limit: Type.Optional(Type.Number({ description: 'Max results (default: 5)' })), - minScore: Type.Optional(Type.Number({ - minimum: 0, - maximum: 1, - description: 'Minimum similarity score 0-1 (default: 0.3)', - })), + query: Type.String({ description: 'Problem to solve' }), + limit: Type.Optional(Type.Number({ description: 'Max results' })), + minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1, description: 'Min similarity 0-1' })), scopeFilter: Type.Optional(ScopeFilterEnum), categoryFilter: Type.Optional(CategoryEnum), - maxComplexity: Type.Optional(Type.Number({ - minimum: 1, - maximum: 10, - description: 'Only return solutions with complexity <= this value', - })), + maxComplexity: Type.Optional(Type.Number({ minimum: 1, maximum: 10, description: 'Max complexity filter' })), }); const CodeBlockSchema = Type.Object({ @@ -114,37 +106,33 @@ const CodeBlockSchema = Type.Object({ }); export const StoreInputSchema = Type.Object({ - problem: Type.String({ description: 'The problem that was solved' }), - solution: Type.String({ description: 'The solution (code, steps, explanation)' }), + problem: Type.String({ description: 'Problem solved' }), + solution: Type.String({ description: 'Solution (code, steps, explanation)' }), scope: ScopeEnum, - tags: Type.Optional(Type.Array(Type.String(), { description: 'Tags for categorization' })), - filesAffected: Type.Optional(Type.Array(Type.String(), { description: 'Files that were modified' })), + tags: Type.Optional(Type.Array(Type.String(), { description: 'Tags' })), + filesAffected: Type.Optional(Type.Array(Type.String(), { description: 'Modified files' })), category: Type.Optional(CategoryEnum), - complexity: Type.Optional(Type.Number({ - minimum: 1, - maximum: 10, - description: 'Complexity 1-10 (auto-calculated if not provided)', - })), - prerequisites: Type.Optional(Type.Array(Type.String(), { description: 'Conditions for this solution to apply' })), + complexity: Type.Optional(Type.Number({ minimum: 1, maximum: 10, description: 'Complexity 1-10' })), + prerequisites: Type.Optional(Type.Array(Type.String(), { description: 'Required conditions' })), antiPatterns: Type.Optional(Type.Array(Type.String(), { description: 'What NOT to do' })), codeBlocks: Type.Optional(Type.Array(CodeBlockSchema, { description: 'Code snippets' })), - relatedSolutions: Type.Optional(Type.Array(Type.String(), { description: 'IDs of related solutions' })), - supersedes: Type.Optional(Type.String({ description: 'ID of solution this replaces' })), + relatedSolutions: Type.Optional(Type.Array(Type.String(), { description: 'Related solution IDs' })), + supersedes: Type.Optional(Type.String({ description: 'Superseded solution ID' })), }); export const RewardInputSchema = Type.Object({ - solutionId: Type.String({ description: 'ID of the solution (from matrix_recall)' }), + solutionId: Type.String({ description: 'Solution ID from matrix_recall' }), outcome: OutcomeEnum, - notes: Type.Optional(Type.String({ description: 'What worked or what needed to change' })), + notes: Type.Optional(Type.String({ description: 'What worked or needed change' })), }); export const FailureInputSchema = Type.Object({ errorType: ErrorTypeEnum, errorMessage: Type.String(), - stackTrace: Type.Optional(Type.String({ description: 'Stack trace if available' })), - rootCause: Type.String({ description: 'What actually caused the error' }), - fixApplied: Type.String({ description: 'How it was fixed' }), - prevention: Type.Optional(Type.String({ description: 'How to avoid this in the future' })), + stackTrace: Type.Optional(Type.String({ description: 'Stack trace' })), + rootCause: Type.String({ description: 'Root cause' }), + fixApplied: Type.String({ description: 'Fix applied' }), + prevention: Type.Optional(Type.String({ description: 'Prevention strategy' })), filesInvolved: Type.Optional(Type.Array(Type.String())), }); @@ -161,97 +149,87 @@ const WarnActionEnum = Type.Union([ export const WarnInputSchema = Type.Object({ action: WarnActionEnum, type: Type.Optional(WarningTypeEnum), - target: Type.Optional(Type.String({ description: 'File path (supports glob patterns) or package name' })), - reason: Type.Optional(Type.String({ description: 'Why this file/package is problematic (required for add action)' })), + target: Type.Optional(Type.String({ description: 'File path/glob or package name' })), + reason: Type.Optional(Type.String({ description: 'Why problematic (for add)' })), severity: Type.Optional(SeverityEnum), ecosystem: Type.Optional(EcosystemEnum), - id: Type.Optional(Type.String({ description: 'Warning ID (for remove action)' })), - repoOnly: Type.Optional(Type.Boolean({ description: 'If true, only show warnings specific to current repository (for list action)' })), - repoSpecific: Type.Optional(Type.Boolean({ description: 'If true, warning only applies to current repository (for add action)' })), + id: Type.Optional(Type.String({ description: 'Warning ID (for remove)' })), + repoOnly: Type.Optional(Type.Boolean({ description: 'Repo-specific only (for list)' })), + repoSpecific: Type.Optional(Type.Boolean({ description: 'Repo-specific warning (for add)' })), }); export const PromptInputSchema = Type.Object({ - rawPrompt: Type.String({ description: 'The original user prompt to analyze' }), + rawPrompt: Type.String({ description: 'User prompt to analyze' }), mode: Type.Optional(PromptModeEnum), - skipClarification: Type.Optional(Type.Boolean({ description: 'If true, skip clarification questions and proceed with assumptions' })), + skipClarification: Type.Optional(Type.Boolean({ description: 'Skip clarification questions' })), }); +// Shared description for repoPath - remove redundancy +const repoPathDesc = 'Repository path'; + export const FindDefinitionInputSchema = Type.Object({ - symbol: Type.String({ description: 'The symbol name to find (e.g., "handleRequest", "UserService")' }), + symbol: Type.String({ description: 'Symbol name (e.g., "handleRequest")' }), kind: Type.Optional(SymbolKindEnum), - file: Type.Optional(Type.String({ description: 'Optional: limit search to a specific file path' })), - repoPath: Type.Optional(Type.String({ description: 'Optional: path to repository to search (defaults to current directory)' })), + file: Type.Optional(Type.String({ description: 'Limit to file' })), + repoPath: Type.Optional(Type.String({ description: repoPathDesc })), }); export const FindCallersInputSchema = Type.Object({ - symbol: Type.String({ description: 'The symbol name to find callers of (e.g., "handleRequest", "UserService")' }), - file: Type.Optional(Type.String({ description: 'Optional: file where the symbol is defined' })), - repoPath: Type.Optional(Type.String({ description: 'Optional: path to repository to search (defaults to current directory)' })), + symbol: Type.String({ description: 'Symbol to find callers of' }), + file: Type.Optional(Type.String({ description: 'File where symbol is defined' })), + repoPath: Type.Optional(Type.String({ description: repoPathDesc })), }); export const ListExportsInputSchema = Type.Object({ - path: Type.Optional(Type.String({ description: 'File or directory path to list exports from (e.g., "src/utils" or "src/index.ts")' })), - repoPath: Type.Optional(Type.String({ description: 'Optional: path to repository to search (defaults to current directory)' })), + path: Type.Optional(Type.String({ description: 'File or directory path' })), + repoPath: Type.Optional(Type.String({ description: repoPathDesc })), }); export const SearchSymbolsInputSchema = Type.Object({ - query: Type.String({ description: 'Partial symbol name to search for (e.g., "handle" finds "handleRequest", "handleError")' }), - limit: Type.Optional(Type.Number({ description: 'Max results (default: 20)' })), - repoPath: Type.Optional(Type.String({ description: 'Optional: path to repository to search (defaults to current directory)' })), + query: Type.String({ description: 'Partial symbol name' }), + limit: Type.Optional(Type.Number({ description: 'Max results' })), + repoPath: Type.Optional(Type.String({ description: repoPathDesc })), }); export const GetImportsInputSchema = Type.Object({ - file: Type.String({ description: 'File path to get imports from' }), - repoPath: Type.Optional(Type.String({ description: 'Optional: path to repository (defaults to current directory)' })), + file: Type.String({ description: 'File path' }), + repoPath: Type.Optional(Type.String({ description: repoPathDesc })), }); export const IndexStatusInputSchema = Type.Object({ - repoPath: Type.Optional(Type.String({ description: 'Optional: path to repository (defaults to current directory)' })), + repoPath: Type.Optional(Type.String({ description: repoPathDesc })), }); export const ReindexInputSchema = Type.Object({ - full: Type.Optional(Type.Boolean({ description: 'Force full reindex, ignoring incremental mode (default: false)' })), - repoPath: Type.Optional(Type.String({ description: 'Optional: path to repository to index (defaults to current directory)' })), + full: Type.Optional(Type.Boolean({ description: 'Force full reindex' })), + repoPath: Type.Optional(Type.String({ description: repoPathDesc })), }); export const RepomixInputSchema = Type.Object({ - target: Type.String({ description: 'GitHub shorthand (owner/repo) or local path' }), - query: Type.String({ description: 'What implementation are you looking for? Used for semantic search.' }), - branch: Type.Optional(Type.String({ description: 'Git branch (default: HEAD/main)' })), - confirmedFiles: Type.Optional(Type.Array(Type.String(), { description: 'Files to pack (from Phase 1 suggestions). Omit for Phase 1 index.' })), - maxTokens: Type.Optional(Type.Number({ description: 'Maximum tokens for packed output (default: 30000)' })), - maxFiles: Type.Optional(Type.Number({ description: 'Maximum files to suggest in Phase 1 (default: 15)' })), - cacheTTLHours: Type.Optional(Type.Number({ description: 'Cache TTL in hours (default: 24)' })), + target: Type.String({ description: 'GitHub owner/repo or local path' }), + query: Type.String({ description: 'What to search for' }), + branch: Type.Optional(Type.String({ description: 'Git branch' })), + confirmedFiles: Type.Optional(Type.Array(Type.String(), { description: 'Files to pack (Phase 2)' })), + maxTokens: Type.Optional(Type.Number({ description: 'Max output tokens' })), + maxFiles: Type.Optional(Type.Number({ description: 'Max files to suggest' })), + cacheTTLHours: Type.Optional(Type.Number({ description: 'Cache TTL hours' })), }); export const DoctorInputSchema = Type.Object({ - autoFix: Type.Optional(Type.Boolean({ description: 'Automatically attempt to fix issues (default: true)' })), + autoFix: Type.Optional(Type.Boolean({ description: 'Auto-fix issues' })), }); // v2.0 Skill Factory Schemas export const SkillCandidatesInputSchema = Type.Object({ - minScore: Type.Optional(Type.Number({ - minimum: 0, - maximum: 1, - description: 'Minimum success rate threshold (default: 0.7)', - })), - minUses: Type.Optional(Type.Number({ - minimum: 1, - description: 'Minimum number of uses (default: 3)', - })), - limit: Type.Optional(Type.Number({ - minimum: 1, - maximum: 50, - description: 'Maximum candidates to return (default: 10)', - })), - excludePromoted: Type.Optional(Type.Boolean({ - description: 'Exclude already promoted solutions (default: true)', - })), + minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1, description: 'Min success rate' })), + minUses: Type.Optional(Type.Number({ minimum: 1, description: 'Min uses' })), + limit: Type.Optional(Type.Number({ minimum: 1, maximum: 50, description: 'Max candidates' })), + excludePromoted: Type.Optional(Type.Boolean({ description: 'Exclude promoted' })), }); export const LinkSkillInputSchema = Type.Object({ - solutionId: Type.String({ description: 'ID of the solution to link' }), - skillPath: Type.String({ description: 'Path to the skill file (e.g., ~/.claude/skills/my-skill.md)' }), + solutionId: Type.String({ description: 'Solution ID to link' }), + skillPath: Type.String({ description: 'Skill file path' }), }); // ============================================================================