From ff88ed96e8f3efc22ceaf29df7b81361786eecab Mon Sep 17 00:00:00 2001 From: ojowwalker77 Date: Sat, 17 Jan 2026 16:03:26 -0300 Subject: [PATCH 1/3] feat(doctor): add comprehensive checks for v2.0+ features Add diagnostic checks for new Matrix features: - Background Jobs: table existence, orphaned job cleanup - Hook Executions: session tracking table - Skills Directory: existence and skill count - Subagent Hooks: toolSearch config validation - Model Delegation: delegation config section - Dreamer Scheduler: tables and platform registration - File Suggestion: script installation and settings.json All checks include auto-fix capabilities where possible. Enhanced issue template with categorized checks and table summary. --- src/tools/doctor.ts | 562 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 553 insertions(+), 9 deletions(-) diff --git a/src/tools/doctor.ts b/src/tools/doctor.ts index e6747a5..a8f33eb 100644 --- a/src/tools/doctor.ts +++ b/src/tools/doctor.ts @@ -3,21 +3,54 @@ * * Checks Matrix plugin health and attempts to auto-fix issues. * If issue is not user-fixable, prompts user to open GitHub issue. + * + * Diagnostics covered: + * - Matrix Directory + * - Database (connection, schema, tables) + * - Configuration (missing sections, auto-upgrade) + * - Config Migration (deprecated tool names) + * - Hooks Installation + * - Code Index + * - Repo Detection + * - Background Jobs System (v2.0+) + * - Hook Executions Table (v2.0+) + * - Skills Directory (v2.0+) + * - Subagent Hooks Config (v2.0+) + * - Model Delegation Config (v2.0+) + * - Dreamer Scheduler (v2.1+) + * - File Suggestion Script (v2.0+) */ -import { existsSync, statSync, readdirSync, mkdirSync } from 'fs'; +import { existsSync, statSync, readdirSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; +import { spawnSync } from 'child_process'; import { runMigrations } from '../db/migrate.js'; import { getDb } from '../db/index.js'; import { getSchemaVersion } from '../db/migrate.js'; -import { getConfig, getConfigPath, saveConfig, clearCache } from '../config/index.js'; +import { getConfig, getConfigPath, saveConfig, clearCache, DEFAULT_CONFIG } from '../config/index.js'; import { fingerprintRepo } from '../repo/index.js'; import { matrixIndexStatus, matrixReindex } from './index-tools.js'; const MATRIX_DIR = join(homedir(), '.claude', 'matrix'); +const CLAUDE_DIR = join(homedir(), '.claude'); +const SKILLS_DIR = join(CLAUDE_DIR, 'skills'); +const FILE_SUGGESTION_PATH = join(CLAUDE_DIR, 'file-suggestion.sh'); +const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json'); const GITHUB_REPO = 'https://github.com/ojowwalker77/Claude-Matrix'; +// Embedded file-suggestion.sh content (same as session-start.ts) +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 +`; + export interface DiagnosticCheck { name: string; status: 'pass' | 'warn' | 'fail'; @@ -347,6 +380,392 @@ function checkRepoDetection(): DiagnosticCheck { } } +/** + * Check background_jobs table exists (v2.0+ feature) + */ +function checkBackgroundJobs(): DiagnosticCheck { + try { + const db = getDb(); + const tableExists = db.query(` + SELECT name FROM sqlite_master + WHERE type='table' AND name='background_jobs' + `).get(); + + if (!tableExists) { + return { + name: 'Background Jobs', + status: 'warn', + message: 'background_jobs table missing (run migrations)', + autoFixable: true, + fixAction: 'Run database migrations', + }; + } + + // Check for orphaned running jobs (jobs that were running when process died) + const orphaned = db.query(` + SELECT COUNT(*) as count FROM background_jobs + WHERE status = 'running' AND pid IS NOT NULL + `).get() as { count: number }; + + if (orphaned.count > 0) { + return { + name: 'Background Jobs', + status: 'warn', + message: orphaned.count + ' orphaned running jobs found', + autoFixable: true, + fixAction: 'Clean up orphaned jobs', + }; + } + + return { + name: 'Background Jobs', + status: 'pass', + message: 'Table exists, no orphaned jobs', + autoFixable: false, + }; + } catch (err) { + return { + name: 'Background Jobs', + status: 'warn', + message: 'Check failed: ' + (err instanceof Error ? err.message : 'Unknown'), + autoFixable: true, + fixAction: 'Run database migrations', + }; + } +} + +/** + * Check hook_executions table exists (v2.0+ feature for one-time hooks) + */ +function checkHookExecutions(): DiagnosticCheck { + try { + const db = getDb(); + const tableExists = db.query(` + SELECT name FROM sqlite_master + WHERE type='table' AND name='hook_executions' + `).get(); + + if (!tableExists) { + return { + name: 'Hook Executions', + status: 'warn', + message: 'hook_executions table missing (run migrations)', + autoFixable: true, + fixAction: 'Run database migrations', + }; + } + + return { + name: 'Hook Executions', + status: 'pass', + message: 'Table exists for session tracking', + autoFixable: false, + }; + } catch (err) { + return { + name: 'Hook Executions', + status: 'warn', + message: 'Check failed: ' + (err instanceof Error ? err.message : 'Unknown'), + autoFixable: true, + fixAction: 'Run database migrations', + }; + } +} + +/** + * Check skills directory exists (v2.0+ feature for hot-reloadable skills) + */ +function checkSkillsDirectory(): DiagnosticCheck { + if (!existsSync(SKILLS_DIR)) { + return { + name: 'Skills Directory', + status: 'warn', + message: 'Skills directory not found: ' + SKILLS_DIR, + autoFixable: true, + fixAction: 'Create skills directory', + }; + } + + try { + const stat = statSync(SKILLS_DIR); + if (!stat.isDirectory()) { + return { + name: 'Skills Directory', + status: 'fail', + message: SKILLS_DIR + ' exists but is not a directory', + autoFixable: false, + }; + } + + // Count skill files + const files = readdirSync(SKILLS_DIR); + const skillDirs = files.filter(f => { + const skillPath = join(SKILLS_DIR, f); + return statSync(skillPath).isDirectory() && existsSync(join(skillPath, 'SKILL.md')); + }); + + return { + name: 'Skills Directory', + status: 'pass', + message: skillDirs.length + ' skills installed', + autoFixable: false, + }; + } catch (err) { + return { + name: 'Skills Directory', + status: 'warn', + message: 'Cannot check skills: ' + (err instanceof Error ? err.message : 'Unknown'), + autoFixable: false, + }; + } +} + +/** + * Check subagent hooks configuration (SubagentStart, SubagentStop) + */ +function checkSubagentHooks(): DiagnosticCheck { + try { + const config = getConfig(); + + // Check if toolSearch config exists (used by subagent hooks) + if (!config.toolSearch) { + return { + name: 'Subagent Hooks', + status: 'warn', + message: 'toolSearch config missing', + autoFixable: true, + fixAction: 'Add toolSearch config section', + }; + } + + // Check if subagent preferences are configured + const hasMatrixIndex = config.toolSearch.preferMatrixIndex !== undefined; + const hasContext7 = config.toolSearch.preferContext7 !== undefined; + + if (!hasMatrixIndex || !hasContext7) { + return { + name: 'Subagent Hooks', + status: 'warn', + message: 'Subagent preferences not configured', + autoFixable: true, + fixAction: 'Add subagent hook preferences', + }; + } + + return { + name: 'Subagent Hooks', + status: 'pass', + message: 'Configured (matrixIndex: ' + config.toolSearch.preferMatrixIndex + ', context7: ' + config.toolSearch.preferContext7 + ')', + autoFixable: false, + }; + } catch (err) { + return { + name: 'Subagent Hooks', + status: 'warn', + message: 'Check failed: ' + (err instanceof Error ? err.message : 'Unknown'), + autoFixable: true, + fixAction: 'Repair configuration', + }; + } +} + +/** + * Check model delegation configuration + */ +function checkDelegation(): DiagnosticCheck { + try { + const config = getConfig(); + + if (!config.delegation) { + return { + name: 'Model Delegation', + status: 'warn', + message: 'delegation config section missing', + autoFixable: true, + fixAction: 'Add delegation config section', + }; + } + + if (config.delegation.enabled === undefined) { + return { + name: 'Model Delegation', + status: 'warn', + message: 'delegation.enabled not set', + autoFixable: true, + fixAction: 'Add delegation.enabled setting', + }; + } + + const model = config.delegation.model || 'haiku'; + return { + name: 'Model Delegation', + status: 'pass', + message: config.delegation.enabled ? 'Enabled (' + model + ')' : 'Disabled', + autoFixable: false, + }; + } catch (err) { + return { + name: 'Model Delegation', + status: 'warn', + message: 'Check failed: ' + (err instanceof Error ? err.message : 'Unknown'), + autoFixable: true, + fixAction: 'Repair configuration', + }; + } +} + +/** + * Check Dreamer scheduler tables and registration + */ +function checkDreamer(): DiagnosticCheck { + try { + const db = getDb(); + + // Check dreamer_tasks table exists + const tasksTable = db.query(` + SELECT name FROM sqlite_master + WHERE type='table' AND name='dreamer_tasks' + `).get(); + + if (!tasksTable) { + return { + name: 'Dreamer Scheduler', + status: 'warn', + message: 'dreamer_tasks table missing (run migrations)', + autoFixable: true, + fixAction: 'Run database migrations', + }; + } + + // Check dreamer_executions table exists + const execsTable = db.query(` + SELECT name FROM sqlite_master + WHERE type='table' AND name='dreamer_executions' + `).get(); + + if (!execsTable) { + return { + name: 'Dreamer Scheduler', + status: 'warn', + message: 'dreamer_executions table missing (run migrations)', + autoFixable: true, + fixAction: 'Run database migrations', + }; + } + + // Count enabled tasks + const taskCount = db.query(` + SELECT COUNT(*) as count FROM dreamer_tasks WHERE enabled = 1 + `).get() as { count: number }; + + // Check platform-specific scheduler health + const platform = process.platform; + let schedulerStatus = 'unknown'; + + if (platform === 'darwin') { + // Check launchd agents directory + const launchAgentsDir = join(homedir(), 'Library', 'LaunchAgents'); + if (existsSync(launchAgentsDir)) { + const files = readdirSync(launchAgentsDir); + const dreamerPlists = files.filter(f => f.startsWith('com.claude.dreamer.')); + schedulerStatus = dreamerPlists.length + ' launchd agents'; + } else { + schedulerStatus = 'LaunchAgents dir missing'; + } + } else if (platform === 'linux') { + // Check crontab + const result = spawnSync('crontab', ['-l'], { encoding: 'utf-8' }); + if (result.status === 0) { + const lines = result.stdout.split('\n').filter(l => l.includes('claude-dreamer')); + schedulerStatus = lines.length + ' cron entries'; + } else { + schedulerStatus = 'crontab not accessible'; + } + } else { + schedulerStatus = 'unsupported platform'; + } + + return { + name: 'Dreamer Scheduler', + status: 'pass', + message: taskCount.count + ' tasks enabled, ' + schedulerStatus, + autoFixable: false, + }; + } catch (err) { + return { + name: 'Dreamer Scheduler', + status: 'warn', + message: 'Check failed: ' + (err instanceof Error ? err.message : 'Unknown'), + autoFixable: true, + fixAction: 'Run database migrations', + }; + } +} + +/** + * Check file-suggestion.sh installation + */ +function checkFileSuggestion(): DiagnosticCheck { + // Check if script exists + if (!existsSync(FILE_SUGGESTION_PATH)) { + return { + name: 'File Suggestion', + status: 'warn', + message: 'file-suggestion.sh not installed', + autoFixable: true, + fixAction: 'Install file-suggestion.sh', + }; + } + + // Check if settings.json references it + try { + if (existsSync(SETTINGS_PATH)) { + const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); + if (!settings.fileSuggestion) { + return { + name: 'File Suggestion', + status: 'warn', + message: 'Script exists but not configured in settings.json', + autoFixable: true, + fixAction: 'Configure in settings.json', + }; + } + } + } catch { + // Ignore settings.json parse errors + } + + // Check if prerequisites are available (rg, fzf, jq) + const checkCommand = (cmd: string): boolean => { + const result = spawnSync('which', [cmd], { encoding: 'utf-8' }); + return result.status === 0; + }; + + const hasRg = checkCommand('rg'); + const hasFzf = checkCommand('fzf'); + const hasJq = checkCommand('jq'); + + if (!hasRg || !hasFzf || !hasJq) { + const missing: string[] = []; + if (!hasRg) missing.push('rg'); + if (!hasFzf) missing.push('fzf'); + if (!hasJq) missing.push('jq'); + return { + name: 'File Suggestion', + status: 'warn', + message: 'Script installed but missing: ' + missing.join(', ') + ' (brew install ' + missing.join(' ') + ')', + autoFixable: false, + }; + } + + return { + name: 'File Suggestion', + status: 'pass', + message: 'Installed and configured', + autoFixable: false, + }; +} + /** * Attempt to auto-fix an issue */ @@ -412,6 +831,68 @@ async function attemptFix(check: DiagnosticCheck): Promise { await matrixReindex({ full: true }); return { ...check, status: 'pass', fixed: true, message: 'Index rebuilt' }; + case 'Background Jobs': + if (check.message.includes('orphaned')) { + // Clean up orphaned running jobs + const db = getDb(); + db.query(` + UPDATE background_jobs + SET status = 'failed', error = 'Orphaned job cleaned up by doctor', completed_at = datetime('now') + WHERE status = 'running' AND pid IS NOT NULL + `).run(); + return { ...check, status: 'pass', fixed: true, message: 'Orphaned jobs cleaned up' }; + } + // Table missing - migrations will fix + runMigrations(); + return { ...check, status: 'pass', fixed: true, message: 'Table created via migrations' }; + + case 'Hook Executions': + // Table missing - migrations will fix + runMigrations(); + return { ...check, status: 'pass', fixed: true, message: 'Table created via migrations' }; + + case 'Skills Directory': + mkdirSync(SKILLS_DIR, { recursive: true }); + return { ...check, status: 'pass', fixed: true, message: 'Skills directory created' }; + + case 'Subagent Hooks': + case 'Model Delegation': { + // Fix by merging config with defaults + clearCache(); + const configToFix = getConfig(); + saveConfig(configToFix); + return { ...check, status: 'pass', fixed: true, message: 'Config updated with defaults' }; + } + + case 'Dreamer Scheduler': + // Table missing - migrations will fix + runMigrations(); + return { ...check, status: 'pass', fixed: true, message: 'Tables created via migrations' }; + + case 'File Suggestion': { + // Install script if missing + if (!existsSync(FILE_SUGGESTION_PATH)) { + writeFileSync(FILE_SUGGESTION_PATH, FILE_SUGGESTION_SCRIPT, { mode: 0o755 }); + } + // Update settings.json if needed + let settings: Record = {}; + if (existsSync(SETTINGS_PATH)) { + try { + settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); + } catch { + settings = {}; + } + } + if (!('fileSuggestion' in settings)) { + settings.fileSuggestion = { + type: 'command', + command: '~/.claude/file-suggestion.sh', + }; + writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2)); + } + return { ...check, status: 'pass', fixed: true, message: 'Script installed and configured' }; + } + default: return check; } @@ -429,14 +910,37 @@ async function attemptFix(check: DiagnosticCheck): Promise { */ function generateIssueTemplate(result: DoctorResult): string { const failedChecks = result.checks.filter(c => c.status === 'fail' && !c.fixed); + const warnChecks = result.checks.filter(c => c.status === 'warn' && !c.fixed); - if (failedChecks.length === 0) { + if (failedChecks.length === 0 && warnChecks.length === 0) { return ''; } - const checksSummary = failedChecks - .map(c => '- **' + c.name + '**: ' + c.message) - .join('\n'); + const failedSummary = failedChecks.length > 0 + ? failedChecks.map(c => '- **' + c.name + '**: ' + c.message).join('\n') + : '_None_'; + + const warnSummary = warnChecks.length > 0 + ? warnChecks.map(c => '- **' + c.name + '**: ' + c.message).join('\n') + : '_None_'; + + // Categorize checks for clearer reporting + const categories = { + core: ['Matrix Directory', 'Database', 'Configuration', 'Config Migration'], + database: ['Background Jobs', 'Hook Executions', 'Dreamer Scheduler'], + hooks: ['Hooks', 'Subagent Hooks'], + config: ['Model Delegation'], + features: ['Code Index', 'Skills Directory', 'File Suggestion', 'Repo Detection'], + }; + + const affectedCategories = new Set(); + for (const check of [...failedChecks, ...warnChecks]) { + for (const [cat, names] of Object.entries(categories)) { + if (names.includes(check.name)) { + affectedCategories.add(cat); + } + } + } return ` ## Bug Report @@ -444,8 +948,14 @@ function generateIssueTemplate(result: DoctorResult): string { ### Description Matrix plugin diagnostic found issues that could not be auto-fixed. -### Failed Checks -${checksSummary} +### Failed Checks (Critical) +${failedSummary} + +### Warning Checks (Non-Critical) +${warnSummary} + +### Affected Categories +${Array.from(affectedCategories).map(c => '- ' + c).join('\n') || '_None identified_'} ### Environment - **OS**: ${result.environment.os} @@ -454,11 +964,21 @@ ${checksSummary} - **Config Path**: ${result.environment.configPath} - **Database Path**: ${result.environment.dbPath} +### All Checks Summary +| Check | Status | Message | +|-------|--------|---------| +${result.checks.map(c => `| ${c.name} | ${c.status}${c.fixed ? ' (fixed)' : ''} | ${c.message.slice(0, 50)}${c.message.length > 50 ? '...' : ''} |`).join('\n')} + ### Diagnostic Output +
+Full JSON Output + \`\`\`json ${JSON.stringify(result, null, 2)} \`\`\` +
+ --- [Open issue on GitHub](${GITHUB_REPO}/issues/new?template=bug_report.md) `; @@ -470,15 +990,39 @@ ${JSON.stringify(result, null, 2)} export async function matrixDoctor(input: DoctorInput = {}): Promise { const autoFix = input.autoFix ?? true; - // Run all checks + // Run all checks (grouped by category) let checks: DiagnosticCheck[] = [ + // Core infrastructure checkMatrixDir(), checkDatabase(), checkConfig(), checkConfigMigration(), + + // Database tables (v2.0+) + checkBackgroundJobs(), + checkHookExecutions(), + + // Hooks system checkHooks(), + checkSubagentHooks(), + + // Code index checkIndex(), + + // Repo detection checkRepoDetection(), + + // Config sections (v2.0+) + checkDelegation(), + + // Skills system (v2.0+) + checkSkillsDirectory(), + + // Scheduler system (v2.1+) + checkDreamer(), + + // File suggestion (v2.0+) + checkFileSuggestion(), ]; // Attempt auto-fixes if enabled From a8c01b8227b3796935f32faf6f7f04f2335e04a9 Mon Sep 17 00:00:00 2001 From: ojowwalker77 Date: Sat, 17 Jan 2026 23:38:38 -0300 Subject: [PATCH 2/3] chore: add v2.1.3 changelog entry for doctor improvements --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8b472..db04802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to Claude Matrix are documented here. +## [2.1.3] - 2025-01-18 + +### Changed + +#### Doctor - Comprehensive Diagnostics +- **Expanded Health Checks** - Added diagnostics for all v2.0+ features + - Background Jobs: Table existence, orphaned job cleanup + - Hook Executions: Session tracking table validation + - Skills Directory: Existence check and skill count + - Subagent Hooks: `toolSearch` config validation + - Model Delegation: `delegation` config section check + - Dreamer Scheduler: Tables and platform registration (launchd/crontab) + - File Suggestion: Script installation and settings.json config +- **Auto-fix Capabilities** - Most issues can be automatically resolved +- **Enhanced Issue Template** - Categorized checks for better bug reporting + +--- + ## [2.1.1] - 2025-01-17 ### Added From 0c39652c7ad07306f08cfed1b44d19298b393f27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Jan 2026 02:38:50 +0000 Subject: [PATCH 3/3] chore: sync versions to 2.1.3 --- .claude-plugin/marketplace.json | 4 ++-- .claude-plugin/plugin.json | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 619df74..7566a31 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.1.1" + "version": "2.1.3" } ], - "version": "2.1.1" + "version": "2.1.3" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2435a80..3bbd1cd 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.1.1", + "version": "2.1.3", "author": { "name": "Matrix Contributors" }, diff --git a/package.json b/package.json index a1f3143..222b75e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-matrix", - "version": "2.1.1", + "version": "2.1.3", "description": "Claude on Rails Tooling System", "type": "module", "main": "src/index.ts",