From 3b968e0707c9508f7ab2eed23405c006c766edf0 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 20:43:08 +0700 Subject: [PATCH 01/33] fix: resolve multiple issues from paradei/Continuous-Claude-v3 - Issue #169: Replace COUNT(*) FILTER(WHERE) with SUM(CASE WHEN...) in BTQL query - Issue #168: Use spawn context for ProcessPoolExecutor on macOS - Issue #152: Fix relative path resolution using input.cwd in tldr-read-enforcer - Issue #156/157: Port ensureMemoryDaemon() from Python to TypeScript session-start hook --- .claude/hooks/src/session-start-continuity.ts | 46 ++++++++++++++++++- .claude/hooks/src/tldr-read-enforcer.ts | 5 +- opc/scripts/braintrust_analyze.py | 2 +- opc/scripts/cc_math/math_base.py | 19 ++++---- opc/scripts/cc_math/sympy_compute.py | 5 +- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/.claude/hooks/src/session-start-continuity.ts b/.claude/hooks/src/session-start-continuity.ts index 8becbf59..e6a60a61 100644 --- a/.claude/hooks/src/session-start-continuity.ts +++ b/.claude/hooks/src/session-start-continuity.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { execSync } from 'child_process'; +import * as os from 'os'; +import { execSync, spawn } from 'child_process'; interface SessionStartInput { type?: 'startup' | 'resume' | 'clear' | 'compact'; // Legacy field @@ -308,10 +309,53 @@ function getUnmarkedHandoffs(): UnmarkedHandoff[] { } } +// ============================================ +// MEMORY DAEMON: Auto-start on session start +// ============================================ + +function ensureMemoryDaemon(): string | null { + const pidFile = path.join(os.homedir(), '.claude', 'memory-daemon.pid'); + + if (fs.existsSync(pidFile)) { + try { + const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10); + process.kill(pid, 0); + return null; + } catch { + fs.unlinkSync(pidFile); + } + } + + const possibleLocations = [ + path.join(os.homedir(), '.claude', 'opc', 'scripts', 'core', 'memory_daemon.py'), + path.join(os.homedir(), '.claude', 'scripts', 'core', 'memory_daemon.py'), + ]; + + for (const daemonScript of possibleLocations) { + if (fs.existsSync(daemonScript)) { + try { + const child = spawn('uv', ['run', 'python', daemonScript, 'start'], { + detached: true, + stdio: 'ignore', + cwd: path.dirname(path.dirname(path.dirname(daemonScript))), + }); + child.unref(); + return 'Memory daemon: Started'; + } catch (e) { + console.error(`Warning: Failed to start memory daemon: ${e}`); + } + } + } + + return null; +} + async function main() { const input: SessionStartInput = JSON.parse(await readStdin()); const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + ensureMemoryDaemon(); + // Support both 'source' (per docs) and 'type' (legacy) fields const sessionType = input.source || input.type; diff --git a/.claude/hooks/src/tldr-read-enforcer.ts b/.claude/hooks/src/tldr-read-enforcer.ts index 3090da00..90a1cad5 100644 --- a/.claude/hooks/src/tldr-read-enforcer.ts +++ b/.claude/hooks/src/tldr-read-enforcer.ts @@ -10,7 +10,7 @@ */ import { readFileSync, existsSync, statSync } from 'fs'; -import { basename, extname } from 'path'; +import { basename, extname, isAbsolute, join } from 'path'; import { queryDaemonSync, DaemonResponse, trackHookActivitySync } from './daemon-client'; // Search context from smart-search-router @@ -371,7 +371,8 @@ async function main() { return; } - const filePath = input.tool_input.file_path || ''; + const rawFilePath = input.tool_input.file_path || ''; + const filePath = isAbsolute(rawFilePath) ? rawFilePath : join(input.cwd, rawFilePath); // Allow non-code files if (!isCodeFile(filePath)) { diff --git a/opc/scripts/braintrust_analyze.py b/opc/scripts/braintrust_analyze.py index 394f8c1d..f2cf0641 100644 --- a/opc/scripts/braintrust_analyze.py +++ b/opc/scripts/braintrust_analyze.py @@ -323,7 +323,7 @@ def list_sessions(project_id: str, api_key: str, limit: int = 5): MIN(created) as started, MAX(created) as ended, COUNT(*) as span_count, - COUNT(*) FILTER (WHERE span_attributes['type'] = 'tool') as tool_count + SUM(CASE WHEN span_attributes['type'] = 'tool' THEN 1 ELSE 0 END) as tool_count FROM logs GROUP BY root_span_id ORDER BY started DESC diff --git a/opc/scripts/cc_math/math_base.py b/opc/scripts/cc_math/math_base.py index 4a2f7526..0d2ca6b3 100644 --- a/opc/scripts/cc_math/math_base.py +++ b/opc/scripts/cc_math/math_base.py @@ -24,7 +24,7 @@ from collections.abc import Callable from dataclasses import dataclass, field from functools import wraps -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, TypeVar # Type variables for generic decorators T = TypeVar("T") @@ -45,7 +45,7 @@ class MathCommand: category: str description: str latex_template: str | None = None - args: list[Dict[str, Any]] = field(default_factory=list) + args: list[dict[str, Any]] = field(default_factory=list) # Global registry per script - use module-level dict @@ -57,7 +57,7 @@ def math_command( category: str, description: str = "", latex_template: str | None = None, - args: List[Dict[str, Any]] | None = None, + args: list[dict[str, Any]] | None = None, ) -> Callable[[F], F]: """Decorator to register a math command. @@ -130,7 +130,7 @@ def clear_registry() -> None: # ============================================================================= -def format_output(result: dict[str, Any], latex_template: str | None = None) -> Dict[str, Any]: +def format_output(result: dict[str, Any], latex_template: str | None = None) -> dict[str, Any]: """Format computation result as standardized JSON. Output structure: @@ -719,7 +719,7 @@ def register_commands( def run_command( args: argparse.Namespace, registry: dict[str, MathCommand] | None = None -) -> Dict[str, Any]: +) -> dict[str, Any]: """Run command based on parsed arguments. Args: @@ -764,15 +764,14 @@ def safe_compute(func: Callable, *args, timeout: int = 30, **kwargs) -> dict[str Returns: Result dict or error dict """ + import multiprocessing from concurrent.futures import ProcessPoolExecutor from concurrent.futures import TimeoutError as FuturesTimeout - def _wrapper(): - return func(*args, **kwargs) - + ctx = multiprocessing.get_context("spawn") try: - with ProcessPoolExecutor(max_workers=1) as executor: - future = executor.submit(_wrapper) + with ProcessPoolExecutor(max_workers=1, mp_context=ctx) as executor: + future = executor.submit(func, *args, **kwargs) try: return future.result(timeout=timeout) except FuturesTimeout: diff --git a/opc/scripts/cc_math/sympy_compute.py b/opc/scripts/cc_math/sympy_compute.py index 8e222e66..3fc51b33 100644 --- a/opc/scripts/cc_math/sympy_compute.py +++ b/opc/scripts/cc_math/sympy_compute.py @@ -1435,8 +1435,11 @@ def safe_solve(equation: str, var: str = "x", domain: str = "complex", timeout: Returns: dict with solutions on success, or error dict on failure/timeout """ + import multiprocessing + + ctx = multiprocessing.get_context("spawn") try: - with ProcessPoolExecutor(max_workers=1) as executor: + with ProcessPoolExecutor(max_workers=1, mp_context=ctx) as executor: future = executor.submit(_solve_internal, equation, var, domain) try: return future.result(timeout=timeout) From f5067393d4eed72e07c7b905bde259a34e78ee33 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 20:53:35 +0700 Subject: [PATCH 02/33] fix: copy .claude/scripts/ root files during install (issue #154/#155) - Add _copy_dotfile_scripts() to copy root-level .py/.sh files from .claude/scripts/ - Called from both install_opc_integration() and install_opc_integration_symlink() - Add missing 'Installed N scripts' print line in wizard for both install paths - Files now deployed: status.py, recall_learnings.py, tldr_stats.py --- .../hooks/dist/session-start-continuity.mjs | 36 +++- .claude/hooks/dist/tldr-read-enforcer.mjs | 5 +- opc/scripts/setup/claude_integration.py | 63 ++++-- opc/scripts/setup/wizard.py | 187 +++++++++++++----- 4 files changed, 224 insertions(+), 67 deletions(-) diff --git a/.claude/hooks/dist/session-start-continuity.mjs b/.claude/hooks/dist/session-start-continuity.mjs index 671d6630..4e9ae9b6 100644 --- a/.claude/hooks/dist/session-start-continuity.mjs +++ b/.claude/hooks/dist/session-start-continuity.mjs @@ -1,7 +1,8 @@ // src/session-start-continuity.ts import * as fs from "fs"; import * as path from "path"; -import { execSync } from "child_process"; +import * as os from "os"; +import { execSync, spawn } from "child_process"; function buildHandoffDirName(sessionName, sessionId) { const uuidShort = sessionId.replace(/-/g, "").slice(0, 8); return `${sessionName}-${uuidShort}`; @@ -159,9 +160,42 @@ function getUnmarkedHandoffs() { return []; } } +function ensureMemoryDaemon() { + const pidFile = path.join(os.homedir(), ".claude", "memory-daemon.pid"); + if (fs.existsSync(pidFile)) { + try { + const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + process.kill(pid, 0); + return null; + } catch { + fs.unlinkSync(pidFile); + } + } + const possibleLocations = [ + path.join(os.homedir(), ".claude", "opc", "scripts", "core", "memory_daemon.py"), + path.join(os.homedir(), ".claude", "scripts", "core", "memory_daemon.py") + ]; + for (const daemonScript of possibleLocations) { + if (fs.existsSync(daemonScript)) { + try { + const child = spawn("uv", ["run", "python", daemonScript, "start"], { + detached: true, + stdio: "ignore", + cwd: path.dirname(path.dirname(path.dirname(daemonScript))) + }); + child.unref(); + return "Memory daemon: Started"; + } catch (e) { + console.error(`Warning: Failed to start memory daemon: ${e}`); + } + } + } + return null; +} async function main() { const input = JSON.parse(await readStdin()); const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + ensureMemoryDaemon(); const sessionType = input.source || input.type; let message = ""; let additionalContext = ""; diff --git a/.claude/hooks/dist/tldr-read-enforcer.mjs b/.claude/hooks/dist/tldr-read-enforcer.mjs index 2aee9800..9898c839 100644 --- a/.claude/hooks/dist/tldr-read-enforcer.mjs +++ b/.claude/hooks/dist/tldr-read-enforcer.mjs @@ -1,6 +1,6 @@ // src/tldr-read-enforcer.ts import { readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "fs"; -import { basename, extname } from "path"; +import { basename, extname, isAbsolute, join as join2 } from "path"; // src/daemon-client.ts import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs"; @@ -520,7 +520,8 @@ async function main() { console.log("{}"); return; } - const filePath = input.tool_input.file_path || ""; + const rawFilePath = input.tool_input.file_path || ""; + const filePath = isAbsolute(rawFilePath) ? rawFilePath : join2(input.cwd, rawFilePath); if (!isCodeFile(filePath)) { console.log("{}"); return; diff --git a/opc/scripts/setup/claude_integration.py b/opc/scripts/setup/claude_integration.py index 18deaf36..49c067fd 100644 --- a/opc/scripts/setup/claude_integration.py +++ b/opc/scripts/setup/claude_integration.py @@ -382,12 +382,12 @@ def generate_migration_guidance( # Root scripts used by skills/hooks - shared between copy and symlink install ROOT_SCRIPTS = [ - "ast_grep_find.py", # /ast-grep-find skill - "braintrust_analyze.py", # session-end-cleanup hook - "qlty_check.py", # /qlty-check skill + "ast_grep_find.py", # /ast-grep-find skill + "braintrust_analyze.py", # session-end-cleanup hook + "qlty_check.py", # /qlty-check skill "research_implement_pipeline.py", # /mcp-chaining skill - "test_research_pipeline.py", # /mcp-chaining skill - "multi_tool_pipeline.py", # /skill-developer example + "test_research_pipeline.py", # /mcp-chaining skill + "multi_tool_pipeline.py", # /skill-developer example "recall_temporal_facts.py", # /system_overview skill ] @@ -431,6 +431,36 @@ def _copy_scripts(opc_source: Path, target_dir: Path) -> int: return count +def _copy_dotfile_scripts(opc_source: Path, target_dir: Path) -> int: + """Copy root-level scripts from .claude/scripts/ to target. + + These are scripts like status.py and tldr_stats.py that live in + .claude/scripts/ at the repo root, not in opc/scripts/. + + Args: + opc_source: Source OPC .claude directory + target_dir: Target .claude directory + + Returns: + Count of scripts copied + """ + count = 0 + source_scripts = opc_source.parent / ".claude" / "scripts" + target_scripts = target_dir / "scripts" + target_scripts.mkdir(parents=True, exist_ok=True) + + if not source_scripts.exists(): + return 0 + + for pattern in ["*.py", "*.sh"]: + for src in source_scripts.glob(pattern): + dst = target_scripts / src.name + shutil.copy2(src, dst) + count += 1 + + return count + + def install_opc_integration( target_dir: Path, opc_source: Path, @@ -539,6 +569,9 @@ def install_opc_integration( # Copy scripts (core, math, tldr directories + root scripts) result["installed_scripts"] = _copy_scripts(opc_source, target_dir) + # Copy root-level scripts from .claude/scripts/ (e.g. status.py) + result["installed_scripts"] += _copy_dotfile_scripts(opc_source, target_dir) + # Merge user items if requested if merge_user_items and existing and conflicts: # Merge non-conflicting hooks @@ -670,6 +703,9 @@ def install_opc_integration_symlink( # Copy scripts (core, math, tldr directories + root scripts) _copy_scripts(opc_source, target_dir) + # Copy root-level scripts from .claude/scripts/ (e.g. status.py) + _copy_dotfile_scripts(opc_source, target_dir) + result["success"] = True except Exception as e: @@ -754,9 +790,7 @@ def strip_tldr_hooks_from_settings(settings_path: Path) -> bool: if "startup" in matcher or "resume" in matcher: hooks = hook_group.get("hooks", []) new_hooks = [ - h - for h in hooks - if "session-start-tldr-cache" not in h.get("command", "") + h for h in hooks if "session-start-tldr-cache" not in h.get("command", "") ] if len(new_hooks) != len(hooks): modified = True @@ -782,7 +816,6 @@ def strip_tldr_hooks_from_settings(settings_path: Path) -> bool: return False - def get_platform_info() -> dict[str, str]: """Get current platform information. @@ -812,15 +845,15 @@ def find_latest_backup(claude_dir: Path) -> Path | None: # Files to preserve during uninstall (user data accumulated since install) PRESERVE_FILES = [ - "history.jsonl", # Command history - "mcp_config.json", # MCP server configs - ".env", # API keys and settings - "projects.json", # Project configs + "history.jsonl", # Command history + "mcp_config.json", # MCP server configs + ".env", # API keys and settings + "projects.json", # Project configs ] PRESERVE_DIRS = [ - "file-history", # File edit history - "projects", # Project-specific data + "file-history", # File edit history + "projects", # Project-specific data ] diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 142ddc78..db1839b5 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -39,6 +39,7 @@ console = Console() except ImportError: rich_escape = lambda x: x # No escaping needed without Rich + # Fallback for minimal environments class Console: def print(self, *args, **kwargs): @@ -200,7 +201,9 @@ async def check_prerequisites_with_install_offers() -> dict[str, Any]: if not runtime_info["installed"]: await offer_docker_install() elif not runtime_info.get("daemon_running", False): - console.print(f" [yellow]{runtime_name.title()} is installed but the daemon is not running.[/yellow]") + console.print( + f" [yellow]{runtime_name.title()} is installed but the daemon is not running.[/yellow]" + ) if runtime_name == "docker": console.print(" Please start Docker Desktop or the Docker service.") else: @@ -209,24 +212,33 @@ async def check_prerequisites_with_install_offers() -> dict[str, Any]: # Retry loop for daemon startup max_retries = 3 for attempt in range(max_retries): - if Confirm.ask(f"\n Retry checking {runtime_name} daemon? (attempt {attempt + 1}/{max_retries})", default=True): + if Confirm.ask( + f"\n Retry checking {runtime_name} daemon? (attempt {attempt + 1}/{max_retries})", + default=True, + ): console.print(f" Checking {runtime_name} daemon...") await asyncio.sleep(2) # Give daemon time to start runtime_info = await check_runtime_installed(runtime_name) if runtime_info.get("daemon_running", False): result["docker"] = True result["docker_daemon_running"] = True - console.print(f" [green]OK[/green] {runtime_name.title()} daemon is now running!") + console.print( + f" [green]OK[/green] {runtime_name.title()} daemon is now running!" + ) break else: - console.print(f" [yellow]{runtime_name.title()} daemon still not running.[/yellow]") + console.print( + f" [yellow]{runtime_name.title()} daemon still not running.[/yellow]" + ) else: break # Check elan/Lean4 (optional, for theorem proving with /prove skill) if not result["elan"]: console.print("\n [dim]Optional: Lean4/elan not found (needed for /prove skill)[/dim]") - console.print(" [dim]Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh[/dim]") + console.print( + " [dim]Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh[/dim]" + ) # elan is optional, so exclude from all_present check result["all_present"] = all([result["docker"], result["python"], result["uv"]]) @@ -390,7 +402,9 @@ async def prompt_embedding_config() -> dict[str, str]: console.print(" 3. openai - OpenAI API (requires API key)") console.print(" 4. voyage - Voyage AI API (requires API key)") - provider = Prompt.ask("Embedding provider", choices=["local", "ollama", "openai", "voyage"], default="local") + provider = Prompt.ask( + "Embedding provider", choices=["local", "ollama", "openai", "voyage"], default="local" + ) config = {"provider": provider} @@ -448,11 +462,11 @@ def generate_env_file(config: dict[str, Any], env_path: Path) -> None: lines.append(f"# Database Mode: {mode}") if mode == "docker": - host = db.get('host', 'localhost') - port = db.get('port', 5432) - database = db.get('database', 'continuous_claude') - user = db.get('user', 'claude') - password = db.get('password', '') + host = db.get("host", "localhost") + port = db.get("port", 5432) + database = db.get("database", "continuous_claude") + user = db.get("user", "claude") + password = db.get("password", "") lines.append(f"POSTGRES_HOST={host}") lines.append(f"POSTGRES_PORT={port}") lines.append(f"POSTGRES_DB={database}") @@ -461,7 +475,9 @@ def generate_env_file(config: dict[str, Any], env_path: Path) -> None: lines.append(f"POSTGRES_PASSWORD={password}") lines.append("") lines.append("# Connection string for scripts (canonical name)") - lines.append(f"CONTINUOUS_CLAUDE_DB_URL=postgresql://{user}:{password}@{host}:{port}/{database}") + lines.append( + f"CONTINUOUS_CLAUDE_DB_URL=postgresql://{user}:{password}@{host}:{port}/{database}" + ) elif mode == "embedded": pgdata = db.get("pgdata", "") venv = db.get("venv", "") @@ -570,15 +586,24 @@ async def run_setup_wizard() -> None: console.print(" [bold]docker[/bold] - PostgreSQL in Docker (recommended)") console.print(" [bold]embedded[/bold] - Embedded PostgreSQL (no Docker needed)") console.print(" [bold]sqlite[/bold] - SQLite fallback (simplest, no cross-terminal)") - db_mode = Prompt.ask("\n Database mode", choices=["docker", "embedded", "sqlite"], default="docker") + db_mode = Prompt.ask( + "\n Database mode", choices=["docker", "embedded", "sqlite"], default="docker" + ) if db_mode == "embedded": from scripts.setup.embedded_postgres import setup_embedded_environment + console.print(" Setting up embedded postgres (creates Python 3.12 environment)...") embed_result = await setup_embedded_environment() if embed_result["success"]: - console.print(f" [green]OK[/green] Embedded environment ready at {embed_result['venv']}") - db_config = {"mode": "embedded", "pgdata": str(embed_result["pgdata"]), "venv": str(embed_result["venv"])} + console.print( + f" [green]OK[/green] Embedded environment ready at {embed_result['venv']}" + ) + db_config = { + "mode": "embedded", + "pgdata": str(embed_result["pgdata"]), + "venv": str(embed_result["venv"]), + } else: console.print(f" [red]ERROR[/red] {embed_result.get('error', 'Unknown')}") console.print(" Falling back to Docker mode") @@ -586,10 +611,14 @@ async def run_setup_wizard() -> None: if db_mode == "sqlite": db_config = {"mode": "sqlite"} - console.print(" [yellow]Note:[/yellow] Cross-terminal coordination disabled in SQLite mode") + console.print( + " [yellow]Note:[/yellow] Cross-terminal coordination disabled in SQLite mode" + ) if db_mode == "docker": - console.print(" [dim]Customize host/port for containers (podman, nerdctl) or remote postgres.[/dim]") + console.print( + " [dim]Customize host/port for containers (podman, nerdctl) or remote postgres.[/dim]" + ) if Confirm.ask("Configure database connection?", default=True): db_config = await prompt_database_config() password = Prompt.ask("Database password", password=True, default="claude_dev") @@ -633,12 +662,19 @@ async def run_setup_wizard() -> None: console.print(" - Build cache and LSP index storage") console.print(" - Real-time agent status") if Confirm.ask(f"Start {runtime} stack (PostgreSQL, Redis)?", default=True): - from scripts.setup.docker_setup import run_migrations, set_container_runtime, start_docker_stack, wait_for_services + from scripts.setup.docker_setup import ( + run_migrations, + set_container_runtime, + start_docker_stack, + wait_for_services, + ) # Set the detected runtime before starting set_container_runtime(runtime) - console.print(f" [dim]Starting containers (first run downloads ~500MB, may take a few minutes)...[/dim]") + console.print( + f" [dim]Starting containers (first run downloads ~500MB, may take a few minutes)...[/dim]" + ) result = await start_docker_stack(env_file=env_path) if result["success"]: console.print(f" [green]OK[/green] {runtime.title()} stack started") @@ -739,7 +775,12 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] Installed {result['installed_skills']} skills") console.print(f" [green]OK[/green] Installed {result['installed_rules']} rules") console.print(f" [green]OK[/green] Installed {result['installed_agents']} agents") - console.print(f" [green]OK[/green] Installed {result['installed_servers']} MCP servers") + console.print( + f" [green]OK[/green] Installed {result['installed_servers']} MCP servers" + ) + console.print( + f" [green]OK[/green] Installed {result['installed_scripts']} scripts" + ) if result["merged_items"]: console.print( f" [green]OK[/green] Merged {len(result['merged_items'])} custom items" @@ -753,7 +794,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") elif choice == "3": @@ -761,9 +804,13 @@ async def run_setup_wizard() -> None: result = install_opc_integration_symlink(claude_dir, opc_source) if result["success"]: - console.print(f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}") + console.print( + f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}" + ) if result["backed_up_dirs"]: - console.print(f" [green]OK[/green] Backed up: {', '.join(result['backed_up_dirs'])}") + console.print( + f" [green]OK[/green] Backed up: {', '.join(result['backed_up_dirs'])}" + ) console.print(" [dim]Changes in ~/.claude/ now sync to repo automatically[/dim]") # Build TypeScript hooks @@ -774,7 +821,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") else: @@ -801,7 +850,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] Installed {result['installed_skills']} skills") console.print(f" [green]OK[/green] Installed {result['installed_rules']} rules") console.print(f" [green]OK[/green] Installed {result['installed_agents']} agents") - console.print(f" [green]OK[/green] Installed {result['installed_servers']} MCP servers") + console.print( + f" [green]OK[/green] Installed {result['installed_servers']} MCP servers" + ) # Build TypeScript hooks console.print(" Building TypeScript hooks...") @@ -811,7 +862,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") elif choice == "2": @@ -819,7 +872,9 @@ async def run_setup_wizard() -> None: result = install_opc_integration_symlink(claude_dir, opc_source) if result["success"]: - console.print(f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}") + console.print( + f" [green]OK[/green] Symlinked: {', '.join(result['symlinked_dirs'])}" + ) console.print(" [dim]Changes in ~/.claude/ now sync to repo automatically[/dim]") # Build TypeScript hooks @@ -830,7 +885,9 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] {build_msg}") else: console.print(f" [yellow]WARN[/yellow] {build_msg}") - console.print(" [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]") + console.print( + " [dim]You can build manually: cd ~/.claude/hooks && npm install && npm run build[/dim]" + ) else: console.print(f" [red]ERROR[/red] {result.get('error', 'Unknown error')}") else: @@ -851,7 +908,9 @@ async def run_setup_wizard() -> None: export_line = f'export CLAUDE_OPC_DIR="{opc_dir}"' if "CLAUDE_OPC_DIR" not in content: with open(shell_config, "a") as f: - f.write(f"\n# Continuous-Claude OPC directory (for skills to find scripts)\n{export_line}\n") + f.write( + f"\n# Continuous-Claude OPC directory (for skills to find scripts)\n{export_line}\n" + ) console.print(f" [green]OK[/green] Added CLAUDE_OPC_DIR to {shell_config.name}") else: console.print(f" [dim]CLAUDE_OPC_DIR already in {shell_config.name}[/dim]") @@ -957,13 +1016,21 @@ async def run_setup_wizard() -> None: timeout=10, ) # Check if this is llm-tldr (has 'tree', 'structure', 'daemon') not tldr-pages - is_llm_tldr = any(cmd in verify_result.stdout for cmd in ["tree", "structure", "daemon"]) + is_llm_tldr = any( + cmd in verify_result.stdout for cmd in ["tree", "structure", "daemon"] + ) if verify_result.returncode == 0 and is_llm_tldr: console.print(" [green]OK[/green] TLDR CLI available") elif verify_result.returncode == 0 and not is_llm_tldr: - console.print(" [yellow]WARN[/yellow] Wrong tldr detected (tldr-pages, not llm-tldr)") - console.print(" [yellow] [/yellow] The 'tldr' command is shadowed by tldr-pages.") - console.print(" [yellow] [/yellow] Uninstall tldr-pages: pip uninstall tldr") + console.print( + " [yellow]WARN[/yellow] Wrong tldr detected (tldr-pages, not llm-tldr)" + ) + console.print( + " [yellow] [/yellow] The 'tldr' command is shadowed by tldr-pages." + ) + console.print( + " [yellow] [/yellow] Uninstall tldr-pages: pip uninstall tldr" + ) console.print(" [yellow] [/yellow] Or use full path: ~/.local/bin/tldr") if is_llm_tldr: @@ -977,14 +1044,15 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [bold]Semantic Search Configuration[/bold]") console.print(" Natural language code search using AI embeddings.") - console.print(" [dim]First run downloads ~1.3GB model and indexes your codebase.[/dim]") + console.print( + " [dim]First run downloads ~1.3GB model and indexes your codebase.[/dim]" + ) console.print(" [dim]Auto-reindexes in background when files change.[/dim]") if Confirm.ask("\n Enable semantic search?", default=True): # Get threshold threshold_str = Prompt.ask( - " Auto-reindex after how many file changes?", - default="20" + " Auto-reindex after how many file changes?", default="20" ) try: threshold = int(threshold_str) @@ -1005,6 +1073,7 @@ async def run_setup_wizard() -> None: has_gpu = False try: import torch + has_gpu = torch.cuda.is_available() or torch.backends.mps.is_available() except ImportError: pass # No torch = assume no GPU @@ -1025,7 +1094,9 @@ async def run_setup_wizard() -> None: settings_path.parent.mkdir(parents=True, exist_ok=True) settings_path.write_text(json.dumps(settings, indent=2)) - console.print(f" [green]OK[/green] Semantic search enabled (threshold: {threshold})") + console.print( + f" [green]OK[/green] Semantic search enabled (threshold: {threshold})" + ) # Offer to pre-download embedding model # Note: We only download the model here, not index any directory. @@ -1035,7 +1106,11 @@ async def run_setup_wizard() -> None: try: # Just load the model to trigger download (no indexing) download_result = subprocess.run( - [sys.executable, "-c", f"from tldr.semantic import get_model; get_model('{model}')"], + [ + sys.executable, + "-c", + f"from tldr.semantic import get_model; get_model('{model}')", + ], capture_output=True, text=True, timeout=timeout, @@ -1052,7 +1127,9 @@ async def run_setup_wizard() -> None: except Exception as e: console.print(f" [yellow]WARN[/yellow] {e}") else: - console.print(" [dim]Model downloads on first use of: tldr semantic index .[/dim]") + console.print( + " [dim]Model downloads on first use of: tldr semantic index .[/dim]" + ) else: console.print(" Semantic search disabled") console.print(" [dim]Enable later in .claude/settings.json[/dim]") @@ -1073,10 +1150,14 @@ async def run_setup_wizard() -> None: console.print(" [dim]Install later with: uv tool install llm-tldr[/dim]") # Ask to disable hooks since they are pre-configured in settings.json - if Confirm.ask("\n Disable TLDR hooks in settings.json? (Avoids crashes if TLDR missing)", default=False): + if Confirm.ask( + "\n Disable TLDR hooks in settings.json? (Avoids crashes if TLDR missing)", + default=False, + ): settings_path = get_global_claude_dir() / "settings.json" if settings_path.exists(): from scripts.setup.claude_integration import strip_tldr_hooks_from_settings + if strip_tldr_hooks_from_settings(settings_path): console.print(" [green]OK[/green] TLDR hooks disabled") @@ -1133,7 +1214,9 @@ async def run_setup_wizard() -> None: # Check elan prerequisite if not shutil.which("elan"): console.print(" [yellow]WARN[/yellow] Lean 4 (elan) not installed") - console.print(" Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh") + console.print( + " Install with: curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh" + ) console.print(" Then re-run the wizard to install Loogle.") else: console.print(" [green]OK[/green] elan found") @@ -1161,7 +1244,9 @@ async def run_setup_wizard() -> None: if result.returncode == 0: console.print(" [green]OK[/green] Updated") else: - console.print(f" [yellow]WARN[/yellow] Update failed: {result.stderr[:100]}") + console.print( + f" [yellow]WARN[/yellow] Update failed: {result.stderr[:100]}" + ) else: console.print(f" Cloning Loogle to {loogle_home}...") loogle_home.parent.mkdir(parents=True, exist_ok=True) @@ -1198,10 +1283,16 @@ async def run_setup_wizard() -> None: else: console.print(f" [red]ERROR[/red] Build failed") console.print(f" {result.stderr[:200]}") - console.print(" You can build manually: cd ~/.local/share/loogle && lake build") + console.print( + " You can build manually: cd ~/.local/share/loogle && lake build" + ) except subprocess.TimeoutExpired: - console.print(" [yellow]WARN[/yellow] Build timed out (this is normal for first build)") - console.print(" Continue building manually: cd ~/.local/share/loogle && lake build") + console.print( + " [yellow]WARN[/yellow] Build timed out (this is normal for first build)" + ) + console.print( + " Continue building manually: cd ~/.local/share/loogle && lake build" + ) except Exception as e: console.print(f" [red]ERROR[/red] {e}") @@ -1254,7 +1345,7 @@ async def run_setup_wizard() -> None: console.print(f" [yellow]WARN[/yellow] loogle_search.py not found at {src_script}") console.print("") - console.print(" [dim]Usage: loogle-search \"Nontrivial _ ↔ _\"[/dim]") + console.print(' [dim]Usage: loogle-search "Nontrivial _ ↔ _"[/dim]') console.print(" [dim]Or use /prove skill which calls it automatically[/dim]") else: console.print(" Skipped Loogle installation") @@ -1344,9 +1435,7 @@ async def main(): # Show menu if no args if len(sys.argv) == 1: - console.print( - Panel.fit("[bold]CLAUDE CONTINUITY KIT v3[/bold]", border_style="blue") - ) + console.print(Panel.fit("[bold]CLAUDE CONTINUITY KIT v3[/bold]", border_style="blue")) console.print("\n[bold]Options:[/bold]") console.print(" [bold]1[/bold] - Install / Update") console.print(" [bold]2[/bold] - Uninstall (restore backup)") From 77d331691dff4a4add95cf29b2600c2c0959cd2a Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 20:57:14 +0700 Subject: [PATCH 03/33] fix: add fish shell support and cleanup CLAUDE_OPC_DIR on uninstall - Add fish shell detection (use 'set -gx' syntax, ~/.config/fish/config.fish) - Create ~/.config/fish/ directory if it doesn't exist - Remove CLAUDE_OPC_DIR from shell config on uninstall (issue #147) --- opc/scripts/setup/claude_integration.py | 26 +++++++++++++++++++++++++ opc/scripts/setup/wizard.py | 18 +++++++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/opc/scripts/setup/claude_integration.py b/opc/scripts/setup/claude_integration.py index 49c067fd..158d10c0 100644 --- a/opc/scripts/setup/claude_integration.py +++ b/opc/scripts/setup/claude_integration.py @@ -964,6 +964,32 @@ def uninstall_opc_integration( if result["preserved"]: msg_parts.append(f" Preserved user data: {', '.join(result['preserved'])}") + # Clean up CLAUDE_OPC_DIR from shell configs + import os as _os + + shell = _os.environ.get("SHELL", "") + configs_to_clean = [] + if "zsh" in shell: + configs_to_clean.append(_os.path.expanduser("~/.zshrc")) + elif "bash" in shell: + configs_to_clean.append(_os.path.expanduser("~/.bashrc")) + elif "fish" in shell: + fish_config = _os.path.expanduser("~/.config/fish/config.fish") + if _os.path.exists(fish_config): + configs_to_clean.append(fish_config) + + for config_path in configs_to_clean: + try: + with open(config_path, "r") as f: + lines = f.readlines() + new_lines = [l for l in lines if "CLAUDE_OPC_DIR" not in l] + if len(new_lines) < len(lines): + with open(config_path, "w") as f: + f.writelines(new_lines) + msg_parts.append(f" Removed CLAUDE_OPC_DIR from {_os.path.basename(config_path)}") + except Exception: + pass + result["message"] = "\n".join(msg_parts) result["success"] = True return result diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index db1839b5..d524c717 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -896,20 +896,30 @@ async def run_setup_wizard() -> None: # Set CLAUDE_OPC_DIR environment variable for skills to find scripts console.print(" Setting CLAUDE_OPC_DIR environment variable...") shell_config = None + shell_export = None shell = os.environ.get("SHELL", "") if "zsh" in shell: shell_config = Path.home() / ".zshrc" + shell_export = f'export CLAUDE_OPC_DIR="{opc_dir}"' elif "bash" in shell: shell_config = Path.home() / ".bashrc" + shell_export = f'export CLAUDE_OPC_DIR="{opc_dir}"' + elif "fish" in shell: + fish_config_dir = Path.home() / ".config" / "fish" + fish_config_dir.mkdir(parents=True, exist_ok=True) + shell_config = fish_config_dir / "config.fish" + shell_export = f'set -gx CLAUDE_OPC_DIR "{opc_dir}"' opc_dir = _project_root # Use script location, not cwd (robust if invoked from elsewhere) - if shell_config and shell_config.exists(): - content = shell_config.read_text() - export_line = f'export CLAUDE_OPC_DIR="{opc_dir}"' + if shell_config and (shell_config.exists() or "fish" in shell): + if shell_config.exists(): + content = shell_config.read_text() + else: + content = "" if "CLAUDE_OPC_DIR" not in content: with open(shell_config, "a") as f: f.write( - f"\n# Continuous-Claude OPC directory (for skills to find scripts)\n{export_line}\n" + f"\n# Continuous-Claude OPC directory (for skills to find scripts)\n{shell_export}\n" ) console.print(f" [green]OK[/green] Added CLAUDE_OPC_DIR to {shell_config.name}") else: From 028412969e753d21f4dc915f05b60dba94d48474 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 21:07:11 +0700 Subject: [PATCH 04/33] fix: resolve skill reference issues (issue #158) - Remove 4 archived skills from active list (async-repl-protocol, router-first-architecture, search-hierarchy, search-tools) - Rename 7 underscore directories to kebab-case (continuity_ledger, create_handoff, describe_pr, implement_task, implement_plan, resume_handoff, system_overview) - Fix name: field in implement-task and implement-plan SKILL.md - Remove outdated model pin claude-opus-4-5-20251101 from discovery-interview and research skills --- .claude/skills/async-repl-protocol/SKILL.md | 30 ------- .../SKILL.md | 0 .../SKILL.md | 0 .../SKILL.v6.md | 0 .../{describe_pr => describe-pr}/SKILL.md | 0 .claude/skills/discovery-interview/SKILL.md | 1 - .../SKILL.md | 2 +- .../SKILL.md.backup | 0 .../SKILL.md | 2 +- .claude/skills/research/SKILL.md | 1 - .../SKILL.md | 0 .../skills/router-first-architecture/SKILL.md | 53 ------------ .claude/skills/search-hierarchy/SKILL.md | 85 ------------------- .claude/skills/search-tools/SKILL.md | 72 ---------------- .../SKILL.md | 0 15 files changed, 2 insertions(+), 244 deletions(-) delete mode 100644 .claude/skills/async-repl-protocol/SKILL.md rename .claude/skills/{continuity_ledger => continuity-ledger}/SKILL.md (100%) rename .claude/skills/{create_handoff => create-handoff}/SKILL.md (100%) rename .claude/skills/{create_handoff => create-handoff}/SKILL.v6.md (100%) rename .claude/skills/{describe_pr => describe-pr}/SKILL.md (100%) rename .claude/skills/{implement_plan => implement-plan}/SKILL.md (99%) rename .claude/skills/{implement_plan => implement-plan}/SKILL.md.backup (100%) rename .claude/skills/{implement_task => implement-task}/SKILL.md (99%) rename .claude/skills/{resume_handoff => resume-handoff}/SKILL.md (100%) delete mode 100644 .claude/skills/router-first-architecture/SKILL.md delete mode 100644 .claude/skills/search-hierarchy/SKILL.md delete mode 100644 .claude/skills/search-tools/SKILL.md rename .claude/skills/{system_overview => system-overview}/SKILL.md (100%) diff --git a/.claude/skills/async-repl-protocol/SKILL.md b/.claude/skills/async-repl-protocol/SKILL.md deleted file mode 100644 index 943d63f4..00000000 --- a/.claude/skills/async-repl-protocol/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: async-repl-protocol -description: Async REPL Protocol -user-invocable: false ---- - -# Async REPL Protocol - -When working with Agentica's async REPL harness for testing. - -## Rules - -### 1. Use `await` for Future-returning tools - -```python -content = await view_file(path) # NOT view_file(path) -answer = await ask_memory("...") -``` - -### 2. Single code block per response - -Compute AND return in ONE block. Multiple blocks means only first executes. - -```python -# GOOD: Single block -content = await view_file(path) -return any(c.isdigit() for c in content) - -# BAD: Split blocks (second block never runs) -content = await view_file(path) diff --git a/.claude/skills/continuity_ledger/SKILL.md b/.claude/skills/continuity-ledger/SKILL.md similarity index 100% rename from .claude/skills/continuity_ledger/SKILL.md rename to .claude/skills/continuity-ledger/SKILL.md diff --git a/.claude/skills/create_handoff/SKILL.md b/.claude/skills/create-handoff/SKILL.md similarity index 100% rename from .claude/skills/create_handoff/SKILL.md rename to .claude/skills/create-handoff/SKILL.md diff --git a/.claude/skills/create_handoff/SKILL.v6.md b/.claude/skills/create-handoff/SKILL.v6.md similarity index 100% rename from .claude/skills/create_handoff/SKILL.v6.md rename to .claude/skills/create-handoff/SKILL.v6.md diff --git a/.claude/skills/describe_pr/SKILL.md b/.claude/skills/describe-pr/SKILL.md similarity index 100% rename from .claude/skills/describe_pr/SKILL.md rename to .claude/skills/describe-pr/SKILL.md diff --git a/.claude/skills/discovery-interview/SKILL.md b/.claude/skills/discovery-interview/SKILL.md index ce4ac8c0..517ce079 100644 --- a/.claude/skills/discovery-interview/SKILL.md +++ b/.claude/skills/discovery-interview/SKILL.md @@ -2,7 +2,6 @@ name: discovery-interview description: Deep interview process to transform vague ideas into detailed specs. Works for technical and non-technical users. user-invocable: true -model: claude-opus-4-5-20251101 --- # Discovery Interview diff --git a/.claude/skills/implement_plan/SKILL.md b/.claude/skills/implement-plan/SKILL.md similarity index 99% rename from .claude/skills/implement_plan/SKILL.md rename to .claude/skills/implement-plan/SKILL.md index db53ac4d..bf9b7ae8 100644 --- a/.claude/skills/implement_plan/SKILL.md +++ b/.claude/skills/implement-plan/SKILL.md @@ -1,5 +1,5 @@ --- -name: implement_plan +name: implement-plan description: Implement technical plans from thoughts/shared/plans with verification user-invocable: false --- diff --git a/.claude/skills/implement_plan/SKILL.md.backup b/.claude/skills/implement-plan/SKILL.md.backup similarity index 100% rename from .claude/skills/implement_plan/SKILL.md.backup rename to .claude/skills/implement-plan/SKILL.md.backup diff --git a/.claude/skills/implement_task/SKILL.md b/.claude/skills/implement-task/SKILL.md similarity index 99% rename from .claude/skills/implement_task/SKILL.md rename to .claude/skills/implement-task/SKILL.md index 248a4947..5f0d9483 100644 --- a/.claude/skills/implement_task/SKILL.md +++ b/.claude/skills/implement-task/SKILL.md @@ -1,5 +1,5 @@ --- -name: implement_task +name: implement-task description: Implementation agent that executes a single task and creates handoff on completion user-invocable: false --- diff --git a/.claude/skills/research/SKILL.md b/.claude/skills/research/SKILL.md index 5ffec538..ab247ae7 100644 --- a/.claude/skills/research/SKILL.md +++ b/.claude/skills/research/SKILL.md @@ -1,7 +1,6 @@ --- name: research description: Document codebase as-is with thoughts directory for historical context -model: claude-opus-4-5-20251101 user-invocable: false --- diff --git a/.claude/skills/resume_handoff/SKILL.md b/.claude/skills/resume-handoff/SKILL.md similarity index 100% rename from .claude/skills/resume_handoff/SKILL.md rename to .claude/skills/resume-handoff/SKILL.md diff --git a/.claude/skills/router-first-architecture/SKILL.md b/.claude/skills/router-first-architecture/SKILL.md deleted file mode 100644 index f4947d7b..00000000 --- a/.claude/skills/router-first-architecture/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: router-first-architecture -description: Router-First Architecture -user-invocable: false ---- - -# Router-First Architecture - -Route through domain routers before using individual tools. Routers abstract tool selection. - -## Pattern - -Domain routers (like `math-router`) provide deterministic mapping from user intent to exact CLI commands. Always use the router first; only bypass for edge cases. - -## DO - -- Call `math-router route ""` before any math operation -- Let domain skills co-activate with their router (via `coActivate` in skill-rules.json) -- Trust the router's confidence score; only fall back if `command: null` -- Keep trigger keywords/patterns in skill-rules.json broader than routing patterns - -## DON'T - -- Call individual scripts directly when a router exists -- Duplicate routing logic in individual skills -- Let domain skills bypass their router - -## Co-Activation Pattern - -Domain skills should co-activate with their router: - -```json -{ - "math/abstract-algebra/groups": { - "coActivate": ["math-router"], - "coActivateMode": "always" - } -} -``` - -This ensures the router is always available when domain knowledge is activated. - -## Two-Layer Architecture - -1. **Skill-rules trigger layer**: Nudges Claude to use the router (keywords, intent patterns) -2. **Router routing layer**: Deterministic mapping to scripts via regex patterns - -Keep the trigger layer broader than routing - the router should handle "not found" gracefully. - -## Source Sessions - -- 2bbc8d6e: "Trigger layer was narrower than routing layer" - expanded triggers -- This session: Wired 8 domain math skills to co-activate with math-router diff --git a/.claude/skills/search-hierarchy/SKILL.md b/.claude/skills/search-hierarchy/SKILL.md deleted file mode 100644 index 9fd1ee09..00000000 --- a/.claude/skills/search-hierarchy/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: search-hierarchy -description: Search Tool Hierarchy -user-invocable: false ---- - -# Search Tool Hierarchy - -Use the most token-efficient search tool for each query type. - -## Decision Tree - -``` -Query Type? -├── STRUCTURAL (code patterns) -│ → AST-grep (~50 tokens output) -│ Examples: "def foo", "class Bar", "import X", "@decorator" -│ -├── SEMANTIC (conceptual questions) -│ → LEANN (~100 tokens if path-only) -│ Examples: "how does auth work", "find error handling patterns" -│ -├── LITERAL (exact identifiers) -│ → Grep (variable output) -│ Examples: "TemporalMemory", "check_evocation", regex patterns -│ -└── FULL CONTEXT (need complete understanding) - → Read (1500+ tokens) - Last resort after finding the right file -``` - -## Token Efficiency Comparison - -| Tool | Output Size | Best For | -|------|-------------|----------| -| AST-grep | ~50 tokens | Function/class definitions, imports, decorators | -| LEANN | ~100 tokens | Conceptual questions, architecture, patterns | -| Grep | ~200-2000 | Exact identifiers, regex, file paths | -| Read | ~1500+ | Full understanding after finding the file | - -## Hook Enforcement - -The `grep-to-leann.sh` hook automatically: -1. Detects query type (structural/semantic/literal) -2. Blocks and suggests AST-grep for structural queries -3. Blocks and suggests LEANN for semantic queries -4. Allows literal patterns through to Grep - -## DO - -- Start with AST-grep for code structure questions -- Use LEANN for "how does X work" questions -- Use Grep only for exact identifier matches -- Read files only after finding them via search - -## DON'T - -- Use Grep for conceptual questions (returns nothing) -- Read files before knowing which ones are relevant -- Use Read when AST-grep would give file:line -- Ignore hook suggestions - -## Examples - -```bash -# STRUCTURAL → AST-grep -ast-grep --pattern "async def $FUNC($$$):" --lang python - -# SEMANTIC → LEANN -leann search opc-dev "how does authentication work" --top-k 3 - -# LITERAL → Grep -Grep pattern="check_evocation" path=opc/scripts - -# FULL CONTEXT → Read (after finding file) -Read file_path=opc/scripts/z3_erotetic.py -``` - -## Optimal Flow - -``` -1. AST-grep: "Find async functions" → 3 file:line matches -2. Read: Top match only → Full understanding -3. Skip: 4 irrelevant files → 6000 tokens saved -``` diff --git a/.claude/skills/search-tools/SKILL.md b/.claude/skills/search-tools/SKILL.md deleted file mode 100644 index f9379606..00000000 --- a/.claude/skills/search-tools/SKILL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: search-tools -description: Search Tool Hierarchy -user-invocable: false ---- - -# Search Tool Hierarchy - -When searching code, use this decision tree: - -## Decision Tree - -``` -Need CONCEPTUAL/SEMANTIC search? - (how does X work, find patterns, understand architecture) - → Use LEANN (/leann-search) - embedding-based semantic search - → PreToolUse hook auto-redirects semantic Grep queries - -Need to understand code STRUCTURE? - (find function calls, class usages, refactor patterns) - → Use AST-grep (/ast-grep-find) - -Need to find TEXT in code? - → Use Morph (/morph-search) - 20x faster - → If no Morph API key: fall back to Grep tool - -Simple one-off search? - → Use built-in Grep tool directly -``` - -## Tool Comparison - -| Tool | Best For | Requires | -|------|----------|----------| -| **LEANN** | Semantic search: "how does caching work", "error handling patterns", conceptual queries | Index built | -| **AST-grep** | Structural patterns: "find all calls to `foo()`", refactoring, find usages by type | MCP server | -| **Morph** | Fast text search: "find files mentioning error", grep across codebase | API key | -| **Grep** | Literal patterns, class/function names, regex | Nothing (built-in) | - -## Examples - -**LEANN** (semantic/conceptual): -- "how does authentication work" -- "find error handling patterns" -- "where is rate limiting implemented" - -**AST-grep** (structural): -- "Find all functions that return a Promise" -- "Find all React components using useState" -- "Refactor all imports of X to Y" - -**Morph** (text search): -- "Find all files mentioning 'authentication'" -- "Search for TODO comments" - -**Grep** (literal): -- `class ProviderAdapter` -- `def __init__` -- Regex patterns - -## LEANN Commands - -```bash -# Search with semantic query -leann search opc-dev "how does blackboard communication work" --top-k 5 - -# List available indexes -leann list - -# Rebuild index (when code changes) -leann build opc-dev --docs dir1 dir2 --no-recompute --no-compact --force -``` diff --git a/.claude/skills/system_overview/SKILL.md b/.claude/skills/system-overview/SKILL.md similarity index 100% rename from .claude/skills/system_overview/SKILL.md rename to .claude/skills/system-overview/SKILL.md From 22db469654e8b188b346c837828d364e6b6076d1 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 21:16:25 +0700 Subject: [PATCH 05/33] feat: add qlty and ast-grep installation steps to wizard (issue #144) - Add Step 11/15: qlty code quality tool (70+ linters, 40+ languages) - Add Step 12/15: ast-grep AST-based code search and refactoring - Update TLDR to Step 13/15, Diagnostics to Step 14/15, Loogle to Step 15/15 - Total steps increased from 13 to 15 --- opc/scripts/setup/wizard.py | 98 +++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 4 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index d524c717..70f929cf 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -990,8 +990,98 @@ async def run_setup_wizard() -> None: console.print(" Skipped math features") console.print(" [dim]Install later with: uv sync --extra math[/dim]") - # Step 9: TLDR Code Analysis Tool - console.print("\n[bold]Step 10/13: TLDR Code Analysis Tool[/bold]") + # Step 11: qlty CLI (Universal Code Quality) + console.print("\n[bold]Step 11/15: qlty Code Quality Tool[/bold]") + console.print( + " qlty is a universal code quality tool supporting 70+ linters for 40+ languages." + ) + console.print(" Unlocks: qlty-check, qlty-during-development, fix (deps scope) skills.") + console.print("") + console.print(" [dim]Free and open source - no API key needed.[/dim]") + + if Confirm.ask("\nInstall qlty code quality tool?", default=True): + console.print(" Installing qlty...") + try: + result = subprocess.run( + ["uv", "tool", "install", "qlty"], + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode == 0: + console.print(" [green]OK[/green] qlty installed via uv") + else: + # Fallback to curl install + result = subprocess.run( + ["curl", "-fsSL", "https://qlty.sh/install.sh"], + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode == 0: + install_result = subprocess.run( + result.stdout.strip(), + shell=True, + capture_output=True, + text=True, + timeout=60, + ) + if install_result.returncode == 0: + console.print(" [green]OK[/green] qlty installed") + else: + console.print(" [yellow]WARN[/yellow] qlty install had issues") + else: + console.print(" [yellow]WARN[/yellow] Could not install qlty") + except subprocess.TimeoutExpired: + console.print(" [yellow]WARN[/yellow] Installation timed out") + except Exception as e: + console.print(f" [yellow]WARN[/yellow] {e}") + else: + console.print(" Skipped qlty installation") + console.print(" [dim]Install later with: uv tool install qlty[/dim]") + + # Step 12: ast-grep (AST-based Code Search) + console.print("\n[bold]Step 12/15: ast-grep Code Analysis Tool[/bold]") + console.print(" ast-grep performs AST-based structural code search and refactoring.") + console.print(" Unlocks: ast-grep-find, search-router, search-tools skills.") + console.print("") + console.print(" [dim]Free and open source - no API key needed.[/dim]") + + if Confirm.ask("\nInstall ast-grep code analysis tool?", default=True): + console.print(" Installing ast-grep...") + try: + result = subprocess.run( + ["cargo", "install", "ast-grep"], + capture_output=True, + text=True, + timeout=300, + ) + if result.returncode == 0: + console.print(" [green]OK[/green] ast-grep installed via cargo") + else: + result = subprocess.run( + ["npm", "install", "-g", "ast-grep"], + capture_output=True, + text=True, + timeout=120, + ) + if result.returncode == 0: + console.print(" [green]OK[/green] ast-grep installed via npm") + else: + console.print(" [yellow]WARN[/yellow] Could not install ast-grep") + console.print(" Try: cargo install ast-grep") + except subprocess.TimeoutExpired: + console.print( + " [yellow]WARN[/yellow] Installation timed out (cargo builds from source)" + ) + except Exception as e: + console.print(f" [yellow]WARN[/yellow] {e}") + else: + console.print(" Skipped ast-grep installation") + console.print(" [dim]Install later with: cargo install ast-grep[/dim]") + + # Step 13: TLDR Code Analysis Tool + console.print("\n[bold]Step 13/15: TLDR Code Analysis Tool[/bold]") console.print(" TLDR provides token-efficient code analysis for LLMs:") console.print(" - 95% token savings vs reading raw files") console.print(" - 155x faster queries with daemon mode") @@ -1172,7 +1262,7 @@ async def run_setup_wizard() -> None: console.print(" [green]OK[/green] TLDR hooks disabled") # Step 10: Diagnostics Tools (Shift-Left Feedback) - console.print("\n[bold]Step 11/13: Diagnostics Tools (Shift-Left Feedback)[/bold]") + console.print("\n[bold]Step 14/15: Diagnostics Tools (Shift-Left Feedback)[/bold]") console.print(" Claude gets immediate type/lint feedback after editing files.") console.print(" This catches errors before tests run (shift-left).") console.print("") @@ -1210,7 +1300,7 @@ async def run_setup_wizard() -> None: console.print(" [dim]TypeScript, Go, Rust coming soon.[/dim]") # Step 11: Loogle (Lean 4 type search for /prove skill) - console.print("\n[bold]Step 12/13: Loogle (Lean 4 Type Search)[/bold]") + console.print("\n[bold]Step 15/15: Loogle (Lean 4 Type Search)[/bold]") console.print(" Loogle enables type-aware search of Mathlib theorems:") console.print(" - Used by /prove skill for theorem proving") console.print(" - Search by type signature (e.g., 'Nontrivial _ ↔ _')") From e2b3c618162b938fc7ffb51e244d528b6e0cfa69 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 21:18:21 +0700 Subject: [PATCH 06/33] feat: add Firecrawl and Morph API keys to wizard (issues #145, #146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Firecrawl API key prompt (web scraping) → FIRECRAWL_API_KEY env var - Add Morph API key prompt (data extraction) → MORPH_API_KEY env var - Unlocks: firecrawl-scrape, research-agent, research-external skills --- opc/scripts/setup/wizard.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 70f929cf..014b1431 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -421,7 +421,7 @@ async def prompt_api_keys() -> dict[str, str]: """Prompt user for optional API keys. Returns: - dict with keys: perplexity, nia, braintrust + dict with keys: perplexity, nia, braintrust, firecrawl, morph """ console.print("\n[bold]API Keys (optional)[/bold]") console.print("Press Enter to skip any key you don't have.\n") @@ -429,11 +429,15 @@ async def prompt_api_keys() -> dict[str, str]: perplexity = Prompt.ask("Perplexity API key (web search)", default="") nia = Prompt.ask("Nia API key (documentation search)", default="") braintrust = Prompt.ask("Braintrust API key (observability)", default="") + firecrawl = Prompt.ask("Firecrawl API key (web scraping)", default="") + morph = Prompt.ask("Morph API key (data extraction)", default="") return { "perplexity": perplexity, "nia": nia, "braintrust": braintrust, + "firecrawl": firecrawl, + "morph": morph, } @@ -516,6 +520,10 @@ def generate_env_file(config: dict[str, Any], env_path: Path) -> None: lines.append(f"NIA_API_KEY={api_keys['nia']}") if api_keys.get("braintrust"): lines.append(f"BRAINTRUST_API_KEY={api_keys['braintrust']}") + if api_keys.get("firecrawl"): + lines.append(f"FIRECRAWL_API_KEY={api_keys['firecrawl']}") + if api_keys.get("morph"): + lines.append(f"MORPH_API_KEY={api_keys['morph']}") lines.append("") # Write file From 8e77a893bca60db7d9fe625b6e5fda651ba714d0 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 21:50:46 +0700 Subject: [PATCH 07/33] feat: add PreToolUse:Bash MCP directory guard (PR #160) - New hook blocks scripts/(mcp|core)/ commands outside OPC directory - Prevents ModuleNotFoundError when uv run misses pyproject.toml - Returns corrected command with cd prefix suggestion - Fixes #148 --- .claude/hooks/dist/mcp-directory-guard.mjs | 86 +++++++++++++++++ .claude/hooks/src/mcp-directory-guard.ts | 104 +++++++++++++++++++++ .claude/settings.json | 5 + 3 files changed, 195 insertions(+) create mode 100644 .claude/hooks/dist/mcp-directory-guard.mjs create mode 100644 .claude/hooks/src/mcp-directory-guard.ts diff --git a/.claude/hooks/dist/mcp-directory-guard.mjs b/.claude/hooks/dist/mcp-directory-guard.mjs new file mode 100644 index 00000000..e50ce092 --- /dev/null +++ b/.claude/hooks/dist/mcp-directory-guard.mjs @@ -0,0 +1,86 @@ +// src/mcp-directory-guard.ts +import { readFileSync } from "fs"; + +// src/shared/opc-path.ts +import { existsSync } from "fs"; +import { join } from "path"; +function getOpcDir() { + const envOpcDir = process.env.CLAUDE_OPC_DIR; + if (envOpcDir && existsSync(envOpcDir)) { + return envOpcDir; + } + const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + const localOpc = join(projectDir, "opc"); + if (existsSync(localOpc)) { + return localOpc; + } + const homeDir = process.env.HOME || process.env.USERPROFILE || ""; + if (homeDir) { + const globalClaude = join(homeDir, ".claude"); + const globalScripts = join(globalClaude, "scripts", "core"); + if (existsSync(globalScripts)) { + return globalClaude; + } + } + return null; +} + +// src/mcp-directory-guard.ts +var SCRIPT_PATH_PATTERN = /\bscripts\/(mcp|core)\//; +function buildCdPrefixPattern(opcDir) { + const escapedDir = opcDir ? opcDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : ""; + const variants = [ + "\\$CLAUDE_OPC_DIR", + "\\$\\{CLAUDE_OPC_DIR\\}" + ]; + if (escapedDir) { + variants.push(escapedDir); + } + return new RegExp(`^\\s*cd\\s+(${variants.join("|")})\\s*&&`); +} +function main() { + let input; + try { + const stdinContent = readFileSync(0, "utf-8"); + input = JSON.parse(stdinContent); + } catch { + console.log("{}"); + return; + } + if (input.tool_name !== "Bash") { + console.log("{}"); + return; + } + const command = input.tool_input?.command; + if (!command) { + console.log("{}"); + return; + } + if (!SCRIPT_PATH_PATTERN.test(command)) { + console.log("{}"); + return; + } + const opcDir = getOpcDir(); + const cdPrefix = buildCdPrefixPattern(opcDir); + if (cdPrefix.test(command)) { + console.log("{}"); + return; + } + const dirRef = opcDir || "$CLAUDE_OPC_DIR"; + const corrected = `cd ${dirRef} && ${command.trimStart()}`; + const output = { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: `OPC directory guard: commands referencing scripts/(mcp|core)/ must run from the OPC directory so uv can find pyproject.toml. + +Blocked command: + ${command.trim()} + +Corrected command: + ${corrected}` + } + }; + console.log(JSON.stringify(output)); +} +main(); diff --git a/.claude/hooks/src/mcp-directory-guard.ts b/.claude/hooks/src/mcp-directory-guard.ts new file mode 100644 index 00000000..6709b191 --- /dev/null +++ b/.claude/hooks/src/mcp-directory-guard.ts @@ -0,0 +1,104 @@ +/** + * PreToolUse:Bash Hook - OPC Script Directory Guard + * + * Prevents running scripts from `scripts/(mcp|core)/` without first + * changing to $CLAUDE_OPC_DIR. When Claude runs these scripts from the + * wrong directory, `uv run` misses `opc/pyproject.toml` and its + * dependencies, causing ModuleNotFoundError. + * + * Detection: any Bash command referencing `scripts/(mcp|core)/` paths + * Allowed: commands prefixed with `cd $CLAUDE_OPC_DIR &&` (or resolved path) + * Denied: returns corrected command in the reason message + * + * Fixes: #148 + */ + +import { readFileSync } from 'fs'; +import { getOpcDir } from './shared/opc-path.js'; +import type { PreToolUseInput, PreToolUseHookOutput } from './shared/types.js'; + +/** + * Pattern matching scripts/(mcp|core)/ references in Bash commands. + * Captures the path for use in the corrected command suggestion. + */ +const SCRIPT_PATH_PATTERN = /\bscripts\/(mcp|core)\//; + +/** + * Pattern matching a proper cd prefix to OPC dir. + * Accepts: + * cd $CLAUDE_OPC_DIR && + * cd ${CLAUDE_OPC_DIR} && + * cd /resolved/opc/path && + */ +function buildCdPrefixPattern(opcDir: string | null): RegExp { + const escapedDir = opcDir ? opcDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : ''; + // Match: cd && (with flexible whitespace) + const variants = [ + '\\$CLAUDE_OPC_DIR', + '\\$\\{CLAUDE_OPC_DIR\\}', + ]; + if (escapedDir) { + variants.push(escapedDir); + } + return new RegExp(`^\\s*cd\\s+(${variants.join('|')})\\s*&&`); +} + +function main(): void { + let input: PreToolUseInput; + try { + const stdinContent = readFileSync(0, 'utf-8'); + input = JSON.parse(stdinContent) as PreToolUseInput; + } catch { + // Can't read input - allow through + console.log('{}'); + return; + } + + // Only process Bash tool + if (input.tool_name !== 'Bash') { + console.log('{}'); + return; + } + + const command = input.tool_input?.command as string; + if (!command) { + console.log('{}'); + return; + } + + // Check if command references OPC script paths + if (!SCRIPT_PATH_PATTERN.test(command)) { + // No script path reference - allow through + console.log('{}'); + return; + } + + const opcDir = getOpcDir(); + const cdPrefix = buildCdPrefixPattern(opcDir); + + // Check if command already has the correct cd prefix + if (cdPrefix.test(command)) { + console.log('{}'); + return; + } + + // Build corrected command suggestion + const dirRef = opcDir || '$CLAUDE_OPC_DIR'; + const corrected = `cd ${dirRef} && ${command.trimStart()}`; + + const output: PreToolUseHookOutput = { + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'deny', + permissionDecisionReason: + `OPC directory guard: commands referencing scripts/(mcp|core)/ must ` + + `run from the OPC directory so uv can find pyproject.toml.\n\n` + + `Blocked command:\n ${command.trim()}\n\n` + + `Corrected command:\n ${corrected}`, + }, + }; + + console.log(JSON.stringify(output)); +} + +main(); diff --git a/.claude/settings.json b/.claude/settings.json index c575d685..ddb3536e 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -228,6 +228,11 @@ { "matcher": "Bash", "hooks": [ + { + "type": "command", + "command": "node $HOME/.claude/hooks/dist/mcp-directory-guard.mjs", + "timeout": 5 + }, { "type": "command", "command": "node $HOME/.claude/hooks/dist/import-error-detector.mjs", From 527f0c6aff0b32de7b9958501eb4ad5d7d6acb35 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 21:59:15 +0700 Subject: [PATCH 08/33] fix: add CLAUDE_CC_DIR export and path fixes for agentica infrastructure - Add CLAUDE_CC_DIR alongside CLAUDE_OPC_DIR in wizard.py setup - Update 8 agents to use CLAUDE_CC_DIR for project paths - Update 8 skills to use CLAUDE_CC_DIR for project paths - References paradei PR #138 --- .claude/agents/agentica-agent.md | 2 +- .claude/agents/braintrust-analyst.md | 4 +-- .claude/agents/debug-agent.md | 2 +- .claude/agents/memory-extractor.md | 10 +++--- .claude/agents/plan-agent.md | 2 +- .claude/agents/scribe.md | 4 +-- .claude/agents/session-analyst.md | 2 +- .claude/agents/validate-agent.md | 2 +- .claude/skills/commit/SKILL.md | 2 +- .claude/skills/commit/SKILL.v6.md | 2 +- .claude/skills/describe-pr/SKILL.md | 2 +- .claude/skills/git-commits/SKILL.md | 2 +- .claude/skills/math-unified/SKILL.md | 16 ++++----- .claude/skills/recall-reasoning/SKILL.md | 4 +-- .claude/skills/skill-developer/SKILL.md | 16 ++++----- .claude/skills/tldr-stats/SKILL.md | 2 +- opc/scripts/setup/wizard.py | 44 ++++++++++++++++++------ 17 files changed, 70 insertions(+), 48 deletions(-) diff --git a/.claude/agents/agentica-agent.md b/.claude/agents/agentica-agent.md index b520a1da..c92fa758 100644 --- a/.claude/agents/agentica-agent.md +++ b/.claude/agents/agentica-agent.md @@ -14,7 +14,7 @@ You are a specialized agent for building Python agents using the Agentica SDK. Y Before starting, read the SDK skill for full API reference: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/agentica-sdk/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/agentica-sdk/SKILL.md ``` ## Step 2: Understand Your Task diff --git a/.claude/agents/braintrust-analyst.md b/.claude/agents/braintrust-analyst.md index 046859fe..e8ac8f6a 100644 --- a/.claude/agents/braintrust-analyst.md +++ b/.claude/agents/braintrust-analyst.md @@ -19,7 +19,7 @@ You are a specialized analysis agent. Your job is to run Braintrust analysis scr Read the braintrust-analyze skill: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/braintrust-analyze/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/braintrust-analyze/SKILL.md ``` ## Step 2: Execute Analysis @@ -27,7 +27,7 @@ cat $CLAUDE_PROJECT_DIR/.claude/skills/braintrust-analyze/SKILL.md Run analysis IMMEDIATELY using Bash tool: ```bash -cd $CLAUDE_PROJECT_DIR && uv run python -m runtime.harness scripts/braintrust_analyze.py --last-session +cd $CLAUDE_OPC_DIR && uv run python -m runtime.harness scripts/braintrust_analyze.py --last-session ``` Other analyses (run as needed): diff --git a/.claude/agents/debug-agent.md b/.claude/agents/debug-agent.md index a27a2e0c..92ffab8e 100644 --- a/.claude/agents/debug-agent.md +++ b/.claude/agents/debug-agent.md @@ -13,7 +13,7 @@ You are a specialized debugging agent. Your job is to investigate issues, trace Before starting, read the debug skill for methodology: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/debug/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/debug/SKILL.md ``` Follow the structure and guidelines from that skill. diff --git a/.claude/agents/memory-extractor.md b/.claude/agents/memory-extractor.md index c20edd25..aa59f791 100644 --- a/.claude/agents/memory-extractor.md +++ b/.claude/agents/memory-extractor.md @@ -31,7 +31,7 @@ You receive: ```bash # Use the extraction script with filtering -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/extract_thinking_blocks.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/extract_thinking_blocks.py \ --jsonl "$JSONL_PATH" \ --filter \ --format json) > /tmp/perception-blocks.json @@ -42,7 +42,7 @@ This extracts only thinking blocks containing perception signals (actually, real ### Step 2: Check Stats ```bash -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/extract_thinking_blocks.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/extract_thinking_blocks.py \ --jsonl "$JSONL_PATH" \ --stats) ``` @@ -80,7 +80,7 @@ For each extracted perception change, use the mapped type from Step 3: ```bash # Example for a CORRECTION → ERROR_FIX -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/store_learning.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/store_learning.py \ --session-id "$SESSION_ID" \ --type "ERROR_FIX" \ --context "what this relates to" \ @@ -90,7 +90,7 @@ For each extracted perception change, use the mapped type from Step 3: --json) # Example for a REALIZATION/INSIGHT → CODEBASE_PATTERN -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/store_learning.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/store_learning.py \ --session-id "$SESSION_ID" \ --type "CODEBASE_PATTERN" \ --context "what this relates to" \ @@ -100,7 +100,7 @@ For each extracted perception change, use the mapped type from Step 3: --json) # Example for a DEBUGGING_APPROACH → WORKING_SOLUTION -(cd $CLAUDE_PROJECT_DIR/opc && uv run python scripts/core/store_learning.py \ +(cd $CLAUDE_OPC_DIR && uv run python scripts/core/store_learning.py \ --session-id "$SESSION_ID" \ --type "WORKING_SOLUTION" \ --context "debugging methodology" \ diff --git a/.claude/agents/plan-agent.md b/.claude/agents/plan-agent.md index f2446f92..3c29bf9d 100644 --- a/.claude/agents/plan-agent.md +++ b/.claude/agents/plan-agent.md @@ -13,7 +13,7 @@ You are a specialized planning agent. Your job is to create detailed implementat Before creating any plan, read the planning skill for methodology and format: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/create_plan/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/create_plan/SKILL.md ``` Follow the structure and guidelines from that skill. diff --git a/.claude/agents/scribe.md b/.claude/agents/scribe.md index 36150db2..53eef639 100644 --- a/.claude/agents/scribe.md +++ b/.claude/agents/scribe.md @@ -15,10 +15,10 @@ Before creating documentation, read the relevant skills: ```bash # For handoffs -cat $CLAUDE_PROJECT_DIR/.claude/skills/create_handoff/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/create_handoff/SKILL.md # For ledger updates -cat $CLAUDE_PROJECT_DIR/.claude/skills/continuity_ledger/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/continuity_ledger/SKILL.md ``` Follow the structure and guidelines from those skills. diff --git a/.claude/agents/session-analyst.md b/.claude/agents/session-analyst.md index 81e0677b..68d965ce 100644 --- a/.claude/agents/session-analyst.md +++ b/.claude/agents/session-analyst.md @@ -13,7 +13,7 @@ You analyze Claude Code session data from Braintrust and provide insights. Read the skill file first: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/braintrust-analyze/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/braintrust-analyze/SKILL.md ``` ## Step 2: Run Analysis diff --git a/.claude/agents/validate-agent.md b/.claude/agents/validate-agent.md index 5b59eb32..fee65d9b 100644 --- a/.claude/agents/validate-agent.md +++ b/.claude/agents/validate-agent.md @@ -13,7 +13,7 @@ You are a specialized validation agent. Your job is to validate a technical plan Before validating, read the validation skill for methodology and format: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/validate-agent/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/validate-agent/SKILL.md ``` Follow the structure and guidelines from that skill. diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md index 1a3c8e35..25cc0081 100644 --- a/.claude/skills/commit/SKILL.md +++ b/.claude/skills/commit/SKILL.md @@ -32,7 +32,7 @@ You are tasked with creating git commits for the changes made during this sessio - Show the result with `git log --oneline -n [number]` 5. **Generate reasoning (after each commit):** - - Run: `bash "$CLAUDE_PROJECT_DIR/.claude/scripts/generate-reasoning.sh" ""` + - Run: `bash "$CLAUDE_CC_DIR/.claude/scripts/generate-reasoning.sh" ""` - This captures what was tried during development (build failures, fixes) - The reasoning file helps future sessions understand past decisions - Stored in `.git/claude/commits//reasoning.md` diff --git a/.claude/skills/commit/SKILL.v6.md b/.claude/skills/commit/SKILL.v6.md index 870a2dd2..735dbc32 100644 --- a/.claude/skills/commit/SKILL.v6.md +++ b/.claude/skills/commit/SKILL.v6.md @@ -71,7 +71,7 @@ eta |-> generate_reasoning(hash, message) ```bash git add ... git commit -m "message" -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/generate-reasoning.sh" "" +bash "$CLAUDE_CC_DIR/.claude/scripts/generate-reasoning.sh" "" git log --oneline -n N ``` diff --git a/.claude/skills/describe-pr/SKILL.md b/.claude/skills/describe-pr/SKILL.md index fbf8c425..20074e87 100644 --- a/.claude/skills/describe-pr/SKILL.md +++ b/.claude/skills/describe-pr/SKILL.md @@ -34,7 +34,7 @@ You are tasked with generating a comprehensive pull request description followin 4b. **Gather reasoning history (if available):** - Check if reasoning files exist: `ls .git/claude/commits/*/reasoning.md 2>/dev/null` - - If they exist, aggregate them: `bash "$CLAUDE_PROJECT_DIR/.claude/scripts/aggregate-reasoning.sh" main` + - If they exist, aggregate them: `bash "$CLAUDE_CC_DIR/.claude/scripts/aggregate-reasoning.sh" main` - This shows what approaches were tried before the final solution - Save the output for inclusion in the PR description diff --git a/.claude/skills/git-commits/SKILL.md b/.claude/skills/git-commits/SKILL.md index cd390434..836aeedf 100644 --- a/.claude/skills/git-commits/SKILL.md +++ b/.claude/skills/git-commits/SKILL.md @@ -38,7 +38,7 @@ When you see these in user prompts, use the commit skill: The skill will prompt you to run: ```bash -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/generate-reasoning.sh" "" +bash "$CLAUDE_CC_DIR/.claude/scripts/generate-reasoning.sh" "" ``` Then push if requested: diff --git a/.claude/skills/math-unified/SKILL.md b/.claude/skills/math-unified/SKILL.md index 70d39795..7bf96c3c 100644 --- a/.claude/skills/math-unified/SKILL.md +++ b/.claude/skills/math-unified/SKILL.md @@ -31,7 +31,7 @@ For formal proofs, use `/prove` instead. ### SymPy (Symbolic Math) ```bash -uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/sympy_compute.py" ``` | Command | Description | Example | @@ -83,7 +83,7 @@ uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/z3_solve.py" ``` | Command | Use Case | @@ -96,7 +96,7 @@ uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/z3_solve.py" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/pint_compute.py" convert ``` Example: `convert 5 miles kilometers` @@ -105,7 +105,7 @@ Example: `convert 5 miles kilometers` ### Math Router (Auto-Route) ```bash -uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/math_router.py" route "" +uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/math_router.py" route "" ``` Returns the exact command to run. Use when unsure which script. @@ -154,28 +154,28 @@ I decide based on your request: ### Solve Equation ``` User: Solve x² - 5x + 6 = 0 -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" solve "x**2 - 5*x + 6" --var x +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/sympy_compute.py" solve "x**2 - 5*x + 6" --var x Result: x = 2 or x = 3 ``` ### Compute Eigenvalues ``` User: Find eigenvalues of [[2, 1], [1, 2]] -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/sympy_compute.py" eigenvalues "[[2,1],[1,2]]" +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/sympy_compute.py" eigenvalues "[[2,1],[1,2]]" Result: {1: 1, 3: 1} (eigenvalue 1 with multiplicity 1, eigenvalue 3 with multiplicity 1) ``` ### Prove Inequality ``` User: Is x² + y² ≥ 2xy always true? -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/z3_solve.py" prove "x**2 + y**2 >= 2*x*y" +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/z3_solve.py" prove "x**2 + y**2 >= 2*x*y" Result: PROVED (equivalent to (x-y)² ≥ 0) ``` ### Convert Units ``` User: How many kilometers in 26.2 miles? -Claude: uv run python "$CLAUDE_PROJECT_DIR/.claude/scripts/cc_math/pint_compute.py" convert 26.2 miles kilometers +Claude: uv run python "$CLAUDE_OPC_DIR/scripts/cc_math/pint_compute.py" convert 26.2 miles kilometers Result: 42.16 km ``` diff --git a/.claude/skills/recall-reasoning/SKILL.md b/.claude/skills/recall-reasoning/SKILL.md index 6110a74a..02c3dce5 100644 --- a/.claude/skills/recall-reasoning/SKILL.md +++ b/.claude/skills/recall-reasoning/SKILL.md @@ -32,7 +32,7 @@ This searches handoffs with post-mortems (what worked, what failed, key decision ### Secondary: Reasoning Files (build attempts) ```bash -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/search-reasoning.sh" "" +bash "$CLAUDE_CC_DIR/.claude/scripts/search-reasoning.sh" "" ``` This searches `.git/claude/commits/*/reasoning.md` for build failures and fixes. @@ -50,7 +50,7 @@ uv run python scripts/core/artifact_query.py "implement agent" --outcome SUCCEED uv run python scripts/core/artifact_query.py "hook implementation" --outcome FAILED # Search build/test reasoning -bash "$CLAUDE_PROJECT_DIR/.claude/scripts/search-reasoning.sh" "TypeError" +bash "$CLAUDE_CC_DIR/.claude/scripts/search-reasoning.sh" "TypeError" ``` ## What Gets Searched diff --git a/.claude/skills/skill-developer/SKILL.md b/.claude/skills/skill-developer/SKILL.md index de41038f..5678ab5d 100644 --- a/.claude/skills/skill-developer/SKILL.md +++ b/.claude/skills/skill-developer/SKILL.md @@ -56,14 +56,14 @@ To create a new MCP chain script and wrap it as a skill: Copy the multi-tool-pipeline template: ```bash -cp $CLAUDE_PROJECT_DIR/scripts/multi_tool_pipeline.py $CLAUDE_PROJECT_DIR/scripts/my_pipeline.py +cp $CLAUDE_OPC_DIR/scripts/multi_tool_pipeline.py $CLAUDE_PROJECT_DIR/scripts/my_pipeline.py ``` Reference the template pattern: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/skills/multi-tool-pipeline/SKILL.md -cat $CLAUDE_PROJECT_DIR/scripts/multi_tool_pipeline.py +cat $CLAUDE_CC_DIR/.claude/skills/multi-tool-pipeline/SKILL.md +cat $CLAUDE_OPC_DIR/scripts/multi_tool_pipeline.py ``` ### Step 2: Customize the Script @@ -140,8 +140,8 @@ Add to `.claude/skills/skill-rules.json`: For full details, read: ```bash -cat $CLAUDE_PROJECT_DIR/.claude/rules/skill-development.md -cat $CLAUDE_PROJECT_DIR/.claude/rules/mcp-scripts.md +cat $CLAUDE_CC_DIR/.claude/rules/skill-development.md +cat $CLAUDE_CC_DIR/.claude/rules/mcp-scripts.md ``` ## Quick Checklist @@ -157,7 +157,7 @@ cat $CLAUDE_PROJECT_DIR/.claude/rules/mcp-scripts.md Look at existing skills for patterns: ```bash -ls $CLAUDE_PROJECT_DIR/.claude/skills/ -cat $CLAUDE_PROJECT_DIR/.claude/skills/commit/SKILL.md -cat $CLAUDE_PROJECT_DIR/.claude/skills/firecrawl-scrape/SKILL.md +ls $CLAUDE_CC_DIR/.claude/skills/ +cat $CLAUDE_CC_DIR/.claude/skills/commit/SKILL.md +cat $CLAUDE_CC_DIR/.claude/skills/firecrawl-scrape/SKILL.md ``` diff --git a/.claude/skills/tldr-stats/SKILL.md b/.claude/skills/tldr-stats/SKILL.md index 6c16e4fd..cb2ed757 100644 --- a/.claude/skills/tldr-stats/SKILL.md +++ b/.claude/skills/tldr-stats/SKILL.md @@ -20,7 +20,7 @@ Show a beautiful dashboard with token usage, actual API costs, TLDR savings, and 1. Run the stats script: ```bash -python3 $CLAUDE_PROJECT_DIR/.claude/scripts/tldr_stats.py +python3 $CLAUDE_CC_DIR/.claude/scripts/tldr_stats.py ``` 2. **Copy the full output into your response** so the user sees the dashboard directly in the chat. Do not just run the command silently - the user wants to see the stats. diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 014b1431..ef6e7e8d 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -901,43 +901,65 @@ async def run_setup_wizard() -> None: else: console.print(" Skipped integration installation") - # Set CLAUDE_OPC_DIR environment variable for skills to find scripts - console.print(" Setting CLAUDE_OPC_DIR environment variable...") + # Set CLAUDE_OPC_DIR and CLAUDE_CC_DIR environment variables + # CLAUDE_OPC_DIR = opc/ directory (Python scripts, MCP runtime) + # CLAUDE_CC_DIR = repository root (contains .claude/scripts/, .claude/skills/) + console.print(" Setting environment variables...") shell_config = None - shell_export = None shell = os.environ.get("SHELL", "") if "zsh" in shell: shell_config = Path.home() / ".zshrc" - shell_export = f'export CLAUDE_OPC_DIR="{opc_dir}"' elif "bash" in shell: shell_config = Path.home() / ".bashrc" - shell_export = f'export CLAUDE_OPC_DIR="{opc_dir}"' elif "fish" in shell: fish_config_dir = Path.home() / ".config" / "fish" fish_config_dir.mkdir(parents=True, exist_ok=True) shell_config = fish_config_dir / "config.fish" - shell_export = f'set -gx CLAUDE_OPC_DIR "{opc_dir}"' opc_dir = _project_root # Use script location, not cwd (robust if invoked from elsewhere) + cc_dir = opc_dir.parent # Repository root (Continuous-Claude-v3/) + if shell_config and (shell_config.exists() or "fish" in shell): if shell_config.exists(): content = shell_config.read_text() else: content = "" + changed = False + export_opc = None + export_cc = None + + if "zsh" in shell or "bash" in shell: + export_opc = f'export CLAUDE_OPC_DIR="{opc_dir}"' + export_cc = f'export CLAUDE_CC_DIR="{cc_dir}"' + elif "fish" in shell: + export_opc = f'set -gx CLAUDE_OPC_DIR "{opc_dir}"' + export_cc = f'set -gx CLAUDE_CC_DIR "{cc_dir}"' + if "CLAUDE_OPC_DIR" not in content: with open(shell_config, "a") as f: - f.write( - f"\n# Continuous-Claude OPC directory (for skills to find scripts)\n{shell_export}\n" - ) - console.print(f" [green]OK[/green] Added CLAUDE_OPC_DIR to {shell_config.name}") + f.write(f"\n# Continuous-Claude directories\n{export_opc}\n{export_cc}\n") + changed = True + elif "CLAUDE_CC_DIR" not in content: + with open(shell_config, "a") as f: + f.write(f"\n# Continuous-Claude root directory\n{export_cc}\n") + changed = True + + if changed: + console.print( + f" [green]OK[/green] Added CLAUDE_OPC_DIR and CLAUDE_CC_DIR to {shell_config.name}" + ) else: - console.print(f" [dim]CLAUDE_OPC_DIR already in {shell_config.name}[/dim]") + console.print( + f" [dim]CLAUDE_OPC_DIR and CLAUDE_CC_DIR already in {shell_config.name}[/dim]" + ) elif sys.platform == "win32": console.print(" [yellow]NOTE[/yellow] Add to your environment:") console.print(f' set CLAUDE_OPC_DIR="{opc_dir}"') + console.print(f' set CLAUDE_CC_DIR="{cc_dir}"') else: console.print(" [yellow]NOTE[/yellow] Add to your shell config:") console.print(f' export CLAUDE_OPC_DIR="{opc_dir}"') + console.print(f' export CLAUDE_CC_DIR="{cc_dir}"') # Step 8: Math Features (Optional) console.print("\n[bold]Step 9/13: Math Features (Optional)[/bold]") From a8a270e04219d320e9f472a66b928bca1dacc3b2 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:20:59 +0700 Subject: [PATCH 09/33] fix: increase qlty install timeout from 60s to 120s --- opc/scripts/setup/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index ef6e7e8d..9a5ff916 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1036,7 +1036,7 @@ async def run_setup_wizard() -> None: ["uv", "tool", "install", "qlty"], capture_output=True, text=True, - timeout=60, + timeout=120, ) if result.returncode == 0: console.print(" [green]OK[/green] qlty installed via uv") From c6073cfb2883d74331cd2faeb4eb36358e6e79df Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:26:29 +0700 Subject: [PATCH 10/33] fix: remove all timeouts from wizard.py install commands - Removes timeout from npm install, npm build - Removes timeout from wait_for_services() - Removes timeout from uv sync, uv tool install, cargo install - Removes timeout from lake build - Removes timeout from model download subprocess calls - Removes related comments --- opc/scripts/setup/wizard.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 9a5ff916..07c0ff81 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -322,7 +322,6 @@ def build_typescript_hooks(hooks_dir: Path) -> tuple[bool, str]: cwd=hooks_dir, capture_output=True, text=True, - timeout=300, ) if result.returncode != 0: return False, f"npm install failed: {result.stderr[:200]}" @@ -334,7 +333,6 @@ def build_typescript_hooks(hooks_dir: Path) -> tuple[bool, str]: cwd=hooks_dir, capture_output=True, text=True, - timeout=120, ) if result.returncode != 0: return False, f"npm build failed: {result.stderr[:200]}" @@ -689,7 +687,7 @@ async def run_setup_wizard() -> None: # Wait for services console.print(" Waiting for services to be healthy...") - health = await wait_for_services(timeout=60) + health = await wait_for_services() if health["all_healthy"]: console.print(" [green]OK[/green] All services healthy") else: @@ -982,7 +980,6 @@ async def run_setup_wizard() -> None: ["uv", "sync", "--extra", "math"], capture_output=True, text=True, - timeout=300, # 5 min timeout for large downloads ) if result.returncode == 0: console.print(" [green]OK[/green] Math packages installed") @@ -999,7 +996,6 @@ async def run_setup_wizard() -> None: ], capture_output=True, text=True, - timeout=30, ) if verify_result.returncode == 0 and "OK" in verify_result.stdout: console.print(" [green]OK[/green] All math imports verified") @@ -1036,7 +1032,6 @@ async def run_setup_wizard() -> None: ["uv", "tool", "install", "qlty"], capture_output=True, text=True, - timeout=120, ) if result.returncode == 0: console.print(" [green]OK[/green] qlty installed via uv") @@ -1046,7 +1041,6 @@ async def run_setup_wizard() -> None: ["curl", "-fsSL", "https://qlty.sh/install.sh"], capture_output=True, text=True, - timeout=30, ) if result.returncode == 0: install_result = subprocess.run( @@ -1054,7 +1048,6 @@ async def run_setup_wizard() -> None: shell=True, capture_output=True, text=True, - timeout=60, ) if install_result.returncode == 0: console.print(" [green]OK[/green] qlty installed") @@ -1084,7 +1077,6 @@ async def run_setup_wizard() -> None: ["cargo", "install", "ast-grep"], capture_output=True, text=True, - timeout=300, ) if result.returncode == 0: console.print(" [green]OK[/green] ast-grep installed via cargo") @@ -1093,7 +1085,6 @@ async def run_setup_wizard() -> None: ["npm", "install", "-g", "ast-grep"], capture_output=True, text=True, - timeout=120, ) if result.returncode == 0: console.print(" [green]OK[/green] ast-grep installed via npm") @@ -1126,12 +1117,10 @@ async def run_setup_wizard() -> None: try: # Install from PyPI using uv tool (puts tldr CLI in PATH) - # Use 300s timeout - first install resolves many deps result = subprocess.run( ["uv", "tool", "install", "llm-tldr"], capture_output=True, text=True, - timeout=300, ) if result.returncode == 0: @@ -1143,7 +1132,6 @@ async def run_setup_wizard() -> None: ["tldr", "--help"], capture_output=True, text=True, - timeout=10, ) # Check if this is llm-tldr (has 'tree', 'structure', 'daemon') not tldr-pages is_llm_tldr = any( @@ -1210,10 +1198,8 @@ async def run_setup_wizard() -> None: if has_gpu: model = "bge-large-en-v1.5" - timeout = 600 # 10 min with GPU else: model = "all-MiniLM-L6-v2" - timeout = 300 # 5 min for small model console.print(" [dim]No GPU detected, using lightweight model[/dim]") settings["semantic_search"] = { @@ -1243,7 +1229,6 @@ async def run_setup_wizard() -> None: ], capture_output=True, text=True, - timeout=timeout, env={**os.environ, "TLDR_AUTO_DOWNLOAD": "1"}, ) if download_result.returncode == 0: @@ -1369,7 +1354,6 @@ async def run_setup_wizard() -> None: cwd=loogle_home, capture_output=True, text=True, - timeout=60, ) if result.returncode == 0: console.print(" [green]OK[/green] Updated") @@ -1385,7 +1369,6 @@ async def run_setup_wizard() -> None: ["git", "clone", "https://github.com/nomeata/loogle", str(loogle_home)], capture_output=True, text=True, - timeout=120, ) if result.returncode == 0: console.print(" [green]OK[/green] Cloned") @@ -1406,7 +1389,6 @@ async def run_setup_wizard() -> None: cwd=loogle_home, capture_output=True, text=True, - timeout=1200, # 20 min ) if result.returncode == 0: console.print(" [green]OK[/green] Loogle built") From 6f598a97226479d5e30dfe124f4bc4c3070be0a4 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:39:23 +0700 Subject: [PATCH 11/33] fix: increase timeouts to reasonable values for slow operations - uv tool install qlty: 120s -> 300s (5 min for first-time install) - curl install script: 30s -> 60s - curl | sh qlty install: 60s -> 120s - cargo install ast-grep: 300s -> 600s (10 min for build from source) - git clone loogle: 120s -> 600s (2GB repo download) These prevent premature timeout failures while still protecting against infinite hangs. --- opc/scripts/setup/wizard.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 07c0ff81..437936c0 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -322,6 +322,7 @@ def build_typescript_hooks(hooks_dir: Path) -> tuple[bool, str]: cwd=hooks_dir, capture_output=True, text=True, + timeout=300, ) if result.returncode != 0: return False, f"npm install failed: {result.stderr[:200]}" @@ -333,6 +334,7 @@ def build_typescript_hooks(hooks_dir: Path) -> tuple[bool, str]: cwd=hooks_dir, capture_output=True, text=True, + timeout=120, ) if result.returncode != 0: return False, f"npm build failed: {result.stderr[:200]}" @@ -687,7 +689,7 @@ async def run_setup_wizard() -> None: # Wait for services console.print(" Waiting for services to be healthy...") - health = await wait_for_services() + health = await wait_for_services(timeout=60) if health["all_healthy"]: console.print(" [green]OK[/green] All services healthy") else: @@ -980,6 +982,7 @@ async def run_setup_wizard() -> None: ["uv", "sync", "--extra", "math"], capture_output=True, text=True, + timeout=300, # 5 min timeout for large downloads ) if result.returncode == 0: console.print(" [green]OK[/green] Math packages installed") @@ -996,6 +999,7 @@ async def run_setup_wizard() -> None: ], capture_output=True, text=True, + timeout=30, ) if verify_result.returncode == 0 and "OK" in verify_result.stdout: console.print(" [green]OK[/green] All math imports verified") @@ -1032,6 +1036,7 @@ async def run_setup_wizard() -> None: ["uv", "tool", "install", "qlty"], capture_output=True, text=True, + timeout=300, ) if result.returncode == 0: console.print(" [green]OK[/green] qlty installed via uv") @@ -1041,6 +1046,7 @@ async def run_setup_wizard() -> None: ["curl", "-fsSL", "https://qlty.sh/install.sh"], capture_output=True, text=True, + timeout=60, ) if result.returncode == 0: install_result = subprocess.run( @@ -1048,6 +1054,7 @@ async def run_setup_wizard() -> None: shell=True, capture_output=True, text=True, + timeout=120, ) if install_result.returncode == 0: console.print(" [green]OK[/green] qlty installed") @@ -1077,6 +1084,7 @@ async def run_setup_wizard() -> None: ["cargo", "install", "ast-grep"], capture_output=True, text=True, + timeout=600, ) if result.returncode == 0: console.print(" [green]OK[/green] ast-grep installed via cargo") @@ -1085,6 +1093,7 @@ async def run_setup_wizard() -> None: ["npm", "install", "-g", "ast-grep"], capture_output=True, text=True, + timeout=120, ) if result.returncode == 0: console.print(" [green]OK[/green] ast-grep installed via npm") @@ -1117,10 +1126,12 @@ async def run_setup_wizard() -> None: try: # Install from PyPI using uv tool (puts tldr CLI in PATH) + # Use 300s timeout - first install resolves many deps result = subprocess.run( ["uv", "tool", "install", "llm-tldr"], capture_output=True, text=True, + timeout=300, ) if result.returncode == 0: @@ -1132,6 +1143,7 @@ async def run_setup_wizard() -> None: ["tldr", "--help"], capture_output=True, text=True, + timeout=10, ) # Check if this is llm-tldr (has 'tree', 'structure', 'daemon') not tldr-pages is_llm_tldr = any( @@ -1198,8 +1210,10 @@ async def run_setup_wizard() -> None: if has_gpu: model = "bge-large-en-v1.5" + timeout = 600 # 10 min with GPU else: model = "all-MiniLM-L6-v2" + timeout = 300 # 5 min for small model console.print(" [dim]No GPU detected, using lightweight model[/dim]") settings["semantic_search"] = { @@ -1229,6 +1243,7 @@ async def run_setup_wizard() -> None: ], capture_output=True, text=True, + timeout=timeout, env={**os.environ, "TLDR_AUTO_DOWNLOAD": "1"}, ) if download_result.returncode == 0: @@ -1354,6 +1369,7 @@ async def run_setup_wizard() -> None: cwd=loogle_home, capture_output=True, text=True, + timeout=60, ) if result.returncode == 0: console.print(" [green]OK[/green] Updated") @@ -1369,6 +1385,7 @@ async def run_setup_wizard() -> None: ["git", "clone", "https://github.com/nomeata/loogle", str(loogle_home)], capture_output=True, text=True, + timeout=600, # 10 min for 2GB repo ) if result.returncode == 0: console.print(" [green]OK[/green] Cloned") @@ -1389,6 +1406,7 @@ async def run_setup_wizard() -> None: cwd=loogle_home, capture_output=True, text=True, + timeout=1200, # 20 min ) if result.returncode == 0: console.print(" [green]OK[/green] Loogle built") From 18edf7ad888c88c53556388f2a9f13b743af123b Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:44:57 +0700 Subject: [PATCH 12/33] fix: skip Docker stack and migrations for embedded/sqlite modes When user selects 'embedded' or 'sqlite' database mode, skip: - Step 6: Docker stack (no need for Docker containers) - Step 7: Database migrations (handled automatically by embedded or not needed for sqlite) --- opc/scripts/setup/wizard.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 437936c0..08d3b192 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -126,6 +126,19 @@ async def check_container_runtime() -> dict[str, Any]: - version: str | None - Version string - daemon_running: bool - True if service is responding """ + import socket + + def is_port_in_use(port: int, host: str = "localhost") -> bool: + """Check if a port is already in use.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + try: + result = sock.connect_ex((host, port)) + sock.close() + return result == 0 + except (socket.timeout, OSError): + return False + # Try Docker first (most common) result = await check_runtime_installed("docker") if result["installed"]: @@ -663,13 +676,15 @@ async def run_setup_wizard() -> None: console.print(f" [green]OK[/green] Generated {env_path}") # Step 5: Container stack (Sandbox Infrastructure) + # Skip if using embedded PostgreSQL or SQLite runtime = prereqs.get("container_runtime", "docker") console.print(f"\n[bold]Step 6/13: Container Stack (Sandbox Infrastructure)[/bold]") - console.print(" The sandbox requires PostgreSQL and Redis for:") - console.print(" - Agent coordination and scheduling") - console.print(" - Build cache and LSP index storage") - console.print(" - Real-time agent status") - if Confirm.ask(f"Start {runtime} stack (PostgreSQL, Redis)?", default=True): + + if db_mode == "embedded": + console.print(" [dim]Skipped - using embedded PostgreSQL (no Docker needed)[/dim]") + elif db_mode == "sqlite": + console.print(" [dim]Skipped - using SQLite (no Docker needed)[/dim]") + elif Confirm.ask(f"Start {runtime} stack (PostgreSQL, Redis)?", default=True): from scripts.setup.docker_setup import ( run_migrations, set_container_runtime, @@ -700,7 +715,11 @@ async def run_setup_wizard() -> None: # Step 6: Migrations console.print("\n[bold]Step 7/13: Database Setup[/bold]") - if Confirm.ask("Run database migrations?", default=True): + if db_mode == "embedded": + console.print(" [dim]Skipped - embedded PostgreSQL handles migrations automatically[/dim]") + elif db_mode == "sqlite": + console.print(" [dim]Skipped - SQLite does not need migrations[/dim]") + elif Confirm.ask("Run database migrations?", default=True): from scripts.setup.docker_setup import run_migrations, set_container_runtime # Ensure runtime is set (in case step 5 was skipped) From 1b2159c19a0abf5c624572099a90b27cc857a09f Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:48:14 +0700 Subject: [PATCH 13/33] fix: auto-migrate schema for embedded PostgreSQL mode After setting up embedded PostgreSQL: 1. Start the pgserver 2. Run docker/init-schema.sql via run_migrations_direct() 3. Show warnings for optional extensions that failed --- opc/scripts/setup/wizard.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 08d3b192..a06c29a0 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -612,7 +612,10 @@ async def run_setup_wizard() -> None: ) if db_mode == "embedded": - from scripts.setup.embedded_postgres import setup_embedded_environment + from scripts.setup.embedded_postgres import ( + run_migrations_direct, + setup_embedded_environment, + ) console.print(" Setting up embedded postgres (creates Python 3.12 environment)...") embed_result = await setup_embedded_environment() @@ -620,6 +623,36 @@ async def run_setup_wizard() -> None: console.print( f" [green]OK[/green] Embedded environment ready at {embed_result['venv']}" ) + + # Start embedded postgres server + from scripts.setup.embedded_postgres import start_embedded_postgres + + console.print(" Starting embedded PostgreSQL server...") + pgdata = embed_result["pgdata"] + server_result = start_embedded_postgres(pgdata) + if server_result["success"]: + console.print(" [green]OK[/green] PostgreSQL server started") + + # Run schema migration + console.print(" Running database schema...") + schema_path = _project_root / "docker" / "init-schema.sql" + migration_result = run_migrations_direct(server_result["uri"], schema_path) + if migration_result["success"]: + console.print(" [green]OK[/green] Schema applied") + if migration_result.get("warnings"): + for w in migration_result["warnings"]: + console.print(f" [yellow]WARN[/yellow] {w}") + else: + console.print( + f" [red]ERROR[/red] Schema migration failed: {migration_result.get('error', 'Unknown')}" + ) + else: + console.print( + f" [red]ERROR[/red] Failed to start PostgreSQL: {server_result.get('error', 'Unknown')}" + ) + console.print(" Falling back to Docker mode") + db_mode = "docker" + db_config = { "mode": "embedded", "pgdata": str(embed_result["pgdata"]), From 17abed30e935c49ca7b153c48cf99ae47dd617db Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:51:37 +0700 Subject: [PATCH 14/33] fix: use venv Python when starting embedded PostgreSQL start_embedded_postgres() now accepts venv_path parameter and uses venv's Python executable to run pgserver as subprocess when pgserver is not available in the current Python environment. --- opc/scripts/setup/embedded_postgres.py | 75 ++++++++++++++++++++++++-- opc/scripts/setup/wizard.py | 3 +- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/opc/scripts/setup/embedded_postgres.py b/opc/scripts/setup/embedded_postgres.py index 4cc9f6b5..fefb4b06 100644 --- a/opc/scripts/setup/embedded_postgres.py +++ b/opc/scripts/setup/embedded_postgres.py @@ -17,12 +17,14 @@ from urllib.parse import quote_plus -def start_embedded_postgres(pgdata: Path) -> dict[str, Any]: +def start_embedded_postgres(pgdata: Path, venv_path: Path | None = None) -> dict[str, Any]: """Start embedded postgres server using pgserver. Args: pgdata: Directory to store postgres data files. Will be created if it doesn't exist. + venv_path: Optional path to venv with pgserver installed. + If not provided, tries to import pgserver directly. Returns: dict with keys: @@ -31,9 +33,61 @@ def start_embedded_postgres(pgdata: Path) -> dict[str, Any]: - error: str (if failed) - server: PostgresServer instance (for cleanup) """ + import sys + + pgserver = None + python_exe = None + + # Try direct import first try: import pgserver except ImportError: + pass + + # If import failed and venv_path provided, use venv Python + if pgserver is None and venv_path is not None: + if sys.platform == "win32": + python_exe = venv_path / "Scripts" / "python.exe" + else: + python_exe = venv_path / "bin" / "python" + + if python_exe.exists(): + # Use subprocess to run pgserver from venv + import subprocess + import time + + proc = subprocess.Popen( + [str(python_exe), "-m", "pgserver", str(pgdata)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Wait for server to start + time.sleep(3) + + # Check if process is still running + if proc.poll() is not None: + stderr = proc.stderr.read().decode() if proc.stderr else "" + return { + "success": False, + "error": f"pgserver failed to start: {stderr[:200]}", + } + + # Default URI - pgserver uses port 28814 by default + uri = "postgresql://postgres:@127.0.0.1:28814/postgres" + + return { + "success": True, + "uri": uri, + "process": proc, + } + else: + return { + "success": False, + "error": f"Python not found at {python_exe}", + } + + if pgserver is None: return { "success": False, "error": "pgserver not installed. Install with: pip install pgserver", @@ -260,7 +314,9 @@ async def setup_embedded_environment() -> dict[str, Any]: if venv_path.exists() and python_exe.exists(): # Verify pgserver is installed proc = await asyncio.create_subprocess_exec( - str(python_exe), "-c", "import pgserver", + str(python_exe), + "-c", + "import pgserver", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -270,7 +326,11 @@ async def setup_embedded_environment() -> dict[str, Any]: # Create venv with Python 3.12 proc = await asyncio.create_subprocess_exec( - "uv", "venv", str(venv_path), "--python", "3.12", + "uv", + "venv", + str(venv_path), + "--python", + "3.12", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -280,8 +340,13 @@ async def setup_embedded_environment() -> dict[str, Any]: # Install pgserver and psycopg2 proc = await asyncio.create_subprocess_exec( - "uv", "pip", "install", "pgserver", "psycopg2-binary", - "--python", str(python_exe), + "uv", + "pip", + "install", + "pgserver", + "psycopg2-binary", + "--python", + str(python_exe), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index a06c29a0..e29d8501 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -629,7 +629,8 @@ async def run_setup_wizard() -> None: console.print(" Starting embedded PostgreSQL server...") pgdata = embed_result["pgdata"] - server_result = start_embedded_postgres(pgdata) + venv_path = Path(embed_result["venv"]) + server_result = start_embedded_postgres(pgdata, venv_path) if server_result["success"]: console.print(" [green]OK[/green] PostgreSQL server started") From 1499a676773d1f401fc4f206ecb097ac46f86eeb Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:54:59 +0700 Subject: [PATCH 15/33] fix: add venv site-packages to sys.path for pgserver import Instead of running pgserver as subprocess, now adds venv's site-packages to sys.path before importing pgserver module. --- opc/scripts/setup/embedded_postgres.py | 53 ++++++++------------------ 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/opc/scripts/setup/embedded_postgres.py b/opc/scripts/setup/embedded_postgres.py index fefb4b06..b4ff1ab3 100644 --- a/opc/scripts/setup/embedded_postgres.py +++ b/opc/scripts/setup/embedded_postgres.py @@ -34,9 +34,9 @@ def start_embedded_postgres(pgdata: Path, venv_path: Path | None = None) -> dict - server: PostgresServer instance (for cleanup) """ import sys + from pathlib import Path as Pathlib pgserver = None - python_exe = None # Try direct import first try: @@ -44,48 +44,25 @@ def start_embedded_postgres(pgdata: Path, venv_path: Path | None = None) -> dict except ImportError: pass - # If import failed and venv_path provided, use venv Python + # If import failed and venv_path provided, add venv site-packages to sys.path if pgserver is None and venv_path is not None: + # Add venv site-packages to sys.path so we can import pgserver if sys.platform == "win32": - python_exe = venv_path / "Scripts" / "python.exe" + site_packages = venv_path / "Lib" / "site-packages" else: - python_exe = venv_path / "bin" / "python" - - if python_exe.exists(): - # Use subprocess to run pgserver from venv - import subprocess - import time - - proc = subprocess.Popen( - [str(python_exe), "-m", "pgserver", str(pgdata)], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + site_packages = ( + venv_path + / "lib" + / f"python{'.'.join(map(str, sys.version_info[:2]))}" + / "site-packages" ) - # Wait for server to start - time.sleep(3) - - # Check if process is still running - if proc.poll() is not None: - stderr = proc.stderr.read().decode() if proc.stderr else "" - return { - "success": False, - "error": f"pgserver failed to start: {stderr[:200]}", - } - - # Default URI - pgserver uses port 28814 by default - uri = "postgresql://postgres:@127.0.0.1:28814/postgres" - - return { - "success": True, - "uri": uri, - "process": proc, - } - else: - return { - "success": False, - "error": f"Python not found at {python_exe}", - } + if site_packages.exists(): + sys.path.insert(0, str(site_packages)) + try: + import pgserver + except ImportError: + pass if pgserver is None: return { From 8bc5715b5c70c879471635ffdca11c63c39d2db6 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 22:58:39 +0700 Subject: [PATCH 16/33] fix: use subprocess with venv Python to start pgserver Since global Python (3.14) cannot import modules compiled for venv Python (3.12), use subprocess to run pgserver startup script with the correct venv Python interpreter. --- opc/scripts/setup/embedded_postgres.py | 79 +++++++++++++------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/opc/scripts/setup/embedded_postgres.py b/opc/scripts/setup/embedded_postgres.py index b4ff1ab3..bedeb38b 100644 --- a/opc/scripts/setup/embedded_postgres.py +++ b/opc/scripts/setup/embedded_postgres.py @@ -34,54 +34,51 @@ def start_embedded_postgres(pgdata: Path, venv_path: Path | None = None) -> dict - server: PostgresServer instance (for cleanup) """ import sys - from pathlib import Path as Pathlib - pgserver = None - - # Try direct import first - try: - import pgserver - except ImportError: - pass - - # If import failed and venv_path provided, add venv site-packages to sys.path - if pgserver is None and venv_path is not None: - # Add venv site-packages to sys.path so we can import pgserver + # Determine Python executable to use + if venv_path is not None: if sys.platform == "win32": - site_packages = venv_path / "Lib" / "site-packages" + python_exe = venv_path / "Scripts" / "python.exe" else: - site_packages = ( - venv_path - / "lib" - / f"python{'.'.join(map(str, sys.version_info[:2]))}" - / "site-packages" - ) - - if site_packages.exists(): - sys.path.insert(0, str(site_packages)) - try: - import pgserver - except ImportError: - pass + python_exe = venv_path / "bin" / "python" + else: + python_exe = sys.executable + + # Build script to start pgserver and get URI + start_script = f""" +import sys +sys.path.insert(0, '{venv_path}/lib/python3.12/site-packages') +from pgserver import get_server +server = get_server('{pgdata}') +print(server.get_uri()) +""" - if pgserver is None: - return { - "success": False, - "error": "pgserver not installed. Install with: pip install pgserver", - } + import subprocess try: - # Ensure pgdata directory exists - pgdata.mkdir(parents=True, exist_ok=True) - - # Start server (pgserver handles init if needed) - server = pgserver.get_server(str(pgdata)) - uri = server.get_uri() - + proc = subprocess.Popen( + [str(python_exe), "-c", start_script], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = proc.communicate(timeout=30) + + if proc.returncode == 0: + uri = stdout.decode().strip() + return { + "success": True, + "uri": uri, + } + else: + error_msg = stderr.decode().strip() if stderr else "Unknown error" + return { + "success": False, + "error": f"Failed to start pgserver: {error_msg[:200]}", + } + except subprocess.TimeoutExpired: return { - "success": True, - "uri": uri, - "server": server, + "success": False, + "error": "pgserver start timed out", } except Exception as e: return { From 45de4a9fec9b5cb5411deabe09d6fa85e8e8fb7e Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:00:27 +0700 Subject: [PATCH 17/33] fix: correct schema path for embedded postgres migration The docker/ directory is at repo root, not opc/ root. --- opc/scripts/setup/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index e29d8501..27111ceb 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -636,7 +636,7 @@ async def run_setup_wizard() -> None: # Run schema migration console.print(" Running database schema...") - schema_path = _project_root / "docker" / "init-schema.sql" + schema_path = _project_root.parent / "docker" / "init-schema.sql" migration_result = run_migrations_direct(server_result["uri"], schema_path) if migration_result["success"]: console.print(" [green]OK[/green] Schema applied") From 8ce2d2292970757535e422069ff6668b55beb88c Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:05:32 +0700 Subject: [PATCH 18/33] revert: remove auto-migrate for embedded mode Embedded PostgreSQL setup is now simpler - just creates venv. User handles server start and schema migration manually. --- opc/scripts/setup/wizard.py | 40 ++++++------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 27111ceb..63eae136 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -612,10 +612,7 @@ async def run_setup_wizard() -> None: ) if db_mode == "embedded": - from scripts.setup.embedded_postgres import ( - run_migrations_direct, - setup_embedded_environment, - ) + from scripts.setup.embedded_postgres import setup_embedded_environment console.print(" Setting up embedded postgres (creates Python 3.12 environment)...") embed_result = await setup_embedded_environment() @@ -623,37 +620,12 @@ async def run_setup_wizard() -> None: console.print( f" [green]OK[/green] Embedded environment ready at {embed_result['venv']}" ) - - # Start embedded postgres server - from scripts.setup.embedded_postgres import start_embedded_postgres - - console.print(" Starting embedded PostgreSQL server...") - pgdata = embed_result["pgdata"] - venv_path = Path(embed_result["venv"]) - server_result = start_embedded_postgres(pgdata, venv_path) - if server_result["success"]: - console.print(" [green]OK[/green] PostgreSQL server started") - - # Run schema migration - console.print(" Running database schema...") - schema_path = _project_root.parent / "docker" / "init-schema.sql" - migration_result = run_migrations_direct(server_result["uri"], schema_path) - if migration_result["success"]: - console.print(" [green]OK[/green] Schema applied") - if migration_result.get("warnings"): - for w in migration_result["warnings"]: - console.print(f" [yellow]WARN[/yellow] {w}") - else: - console.print( - f" [red]ERROR[/red] Schema migration failed: {migration_result.get('error', 'Unknown')}" - ) - else: - console.print( - f" [red]ERROR[/red] Failed to start PostgreSQL: {server_result.get('error', 'Unknown')}" + console.print( + " [dim]To start server: {venv}/bin/python -c \"from pgserver import get_server; get_server('{pgdata}')\"".format( + venv=embed_result["venv"], pgdata=embed_result["pgdata"] ) - console.print(" Falling back to Docker mode") - db_mode = "docker" - + ) + console.print(" [dim]To run schema: psql -f docker/init-schema.sql ") db_config = { "mode": "embedded", "pgdata": str(embed_result["pgdata"]), From d993534102b83e9a3758c66227d7956ad0dd55ac Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:06:35 +0700 Subject: [PATCH 19/33] fix: update embedded migration message to reflect manual process --- opc/scripts/setup/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 63eae136..54d4779f 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -722,7 +722,7 @@ async def run_setup_wizard() -> None: # Step 6: Migrations console.print("\n[bold]Step 7/13: Database Setup[/bold]") if db_mode == "embedded": - console.print(" [dim]Skipped - embedded PostgreSQL handles migrations automatically[/dim]") + console.print(" [dim]Skipped - run manually after starting server[/dim]") elif db_mode == "sqlite": console.print(" [dim]Skipped - SQLite does not need migrations[/dim]") elif Confirm.ask("Run database migrations?", default=True): From 9e96a9bab936dc54c77234e432454ea737062317 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:17:03 +0700 Subject: [PATCH 20/33] fix: check if qlty is already installed before attempting install --- opc/scripts/setup/wizard.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 54d4779f..68f2d9fb 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1054,7 +1054,16 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [dim]Free and open source - no API key needed.[/dim]") - if Confirm.ask("\nInstall qlty code quality tool?", default=True): + # Check if qlty is already installed + qlty_check = subprocess.run( + ["uv", "tool", "run", "qlty", "--version"], + capture_output=True, + text=True, + timeout=10, + ) + if qlty_check.returncode == 0: + console.print(" [green]OK[/green] qlty is already installed") + elif Confirm.ask("\nInstall qlty code quality tool?", default=True): console.print(" Installing qlty...") try: result = subprocess.run( From 2ea20979e45bbe40cffd80892527688ba8f03e56 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:21:43 +0700 Subject: [PATCH 21/33] fix: also check ~/.qlty/bin for qlty installation --- opc/scripts/setup/wizard.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 68f2d9fb..67ece6f5 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1054,7 +1054,8 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [dim]Free and open source - no API key needed.[/dim]") - # Check if qlty is already installed + # Check if qlty is already installed (via uv tool or ~/.qlty/bin) + qlty_found = False qlty_check = subprocess.run( ["uv", "tool", "run", "qlty", "--version"], capture_output=True, @@ -1062,6 +1063,21 @@ async def run_setup_wizard() -> None: timeout=10, ) if qlty_check.returncode == 0: + qlty_found = True + else: + # Check ~/.qlty/bin directly + qlty_path = Path.home() / ".qlty" / "bin" / "qlty" + if qlty_path.exists(): + qlty_check = subprocess.run( + [str(qlty_path), "--version"], + capture_output=True, + text=True, + timeout=10, + ) + if qlty_check.returncode == 0: + qlty_found = True + + if qlty_found: console.print(" [green]OK[/green] qlty is already installed") elif Confirm.ask("\nInstall qlty code quality tool?", default=True): console.print(" Installing qlty...") From b67dc8c74bf88144c8760cc9cb23af9bdd8cebb6 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:32:07 +0700 Subject: [PATCH 22/33] fix: check qlty as standalone binary, not uv tool qlty is a standalone binary installed to ~/.qlty/bin/qlty, not a uv tool. --- opc/scripts/setup/wizard.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 67ece6f5..cb1cdb17 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1054,31 +1054,13 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [dim]Free and open source - no API key needed.[/dim]") - # Check if qlty is already installed (via uv tool or ~/.qlty/bin) - qlty_found = False - qlty_check = subprocess.run( - ["uv", "tool", "run", "qlty", "--version"], - capture_output=True, - text=True, - timeout=10, - ) - if qlty_check.returncode == 0: - qlty_found = True - else: - # Check ~/.qlty/bin directly - qlty_path = Path.home() / ".qlty" / "bin" / "qlty" - if qlty_path.exists(): - qlty_check = subprocess.run( - [str(qlty_path), "--version"], - capture_output=True, - text=True, - timeout=10, - ) - if qlty_check.returncode == 0: - qlty_found = True - - if qlty_found: + # Check if qlty is already installed (standalone binary, not uv tool) + qlty_path = Path.home() / ".qlty" / "bin" / "qlty" + if qlty_path.exists(): console.print(" [green]OK[/green] qlty is already installed") + elif shutil.which("qlty"): + console.print(" [green]OK[/green] qlty is already installed") + elif Confirm.ask("\nInstall qlty code quality tool?", default=True): elif Confirm.ask("\nInstall qlty code quality tool?", default=True): console.print(" Installing qlty...") try: From 190b5f581e625ff01ae05f3536505f35e58c11b5 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:33:39 +0700 Subject: [PATCH 23/33] fix: use curl install for qlty, not uv tool --- opc/scripts/setup/wizard.py | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index cb1cdb17..c497689b 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1061,46 +1061,35 @@ async def run_setup_wizard() -> None: elif shutil.which("qlty"): console.print(" [green]OK[/green] qlty is already installed") elif Confirm.ask("\nInstall qlty code quality tool?", default=True): - elif Confirm.ask("\nInstall qlty code quality tool?", default=True): - console.print(" Installing qlty...") + console.print(" Installing qlty via curl...") try: result = subprocess.run( - ["uv", "tool", "install", "qlty"], + ["curl", "-fsSL", "https://qlty.sh/install.sh"], capture_output=True, text=True, - timeout=300, + timeout=60, ) if result.returncode == 0: - console.print(" [green]OK[/green] qlty installed via uv") - else: - # Fallback to curl install - result = subprocess.run( - ["curl", "-fsSL", "https://qlty.sh/install.sh"], + install_result = subprocess.run( + result.stdout.strip(), + shell=True, capture_output=True, text=True, - timeout=60, + timeout=120, ) - if result.returncode == 0: - install_result = subprocess.run( - result.stdout.strip(), - shell=True, - capture_output=True, - text=True, - timeout=120, - ) - if install_result.returncode == 0: - console.print(" [green]OK[/green] qlty installed") - else: - console.print(" [yellow]WARN[/yellow] qlty install had issues") + if install_result.returncode == 0: + console.print(" [green]OK[/green] qlty installed") else: - console.print(" [yellow]WARN[/yellow] Could not install qlty") + console.print(" [yellow]WARN[/yellow] qlty install had issues") + else: + console.print(" [yellow]WARN[/yellow] Could not install qlty") except subprocess.TimeoutExpired: console.print(" [yellow]WARN[/yellow] Installation timed out") except Exception as e: console.print(f" [yellow]WARN[/yellow] {e}") else: console.print(" Skipped qlty installation") - console.print(" [dim]Install later with: uv tool install qlty[/dim]") + console.print(" [dim]Install later with: curl -fsSL https://qlty.sh/install.sh | sh[/dim]") # Step 12: ast-grep (AST-based Code Search) console.print("\n[bold]Step 12/15: ast-grep Code Analysis Tool[/bold]") From 9752c0540aeaaff9169f6cf372a9a3ac48fdbd04 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:37:55 +0700 Subject: [PATCH 24/33] fix: add pre-install checks for ast-grep and tldr - ast-grep: check with shutil.which before install - tldr: check if llm-tldr is installed (not tldr-pages) --- opc/scripts/setup/wizard.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index c497689b..555d8146 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1098,7 +1098,10 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [dim]Free and open source - no API key needed.[/dim]") - if Confirm.ask("\nInstall ast-grep code analysis tool?", default=True): + # Check if ast-grep is already installed + if shutil.which("ast-grep"): + console.print(" [green]OK[/green] ast-grep is already installed") + elif Confirm.ask("\nInstall ast-grep code analysis tool?", default=True): console.print(" Installing ast-grep...") try: result = subprocess.run( @@ -1141,7 +1144,22 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [dim]Note: First semantic search downloads ~1.3GB embedding model.[/dim]") - if Confirm.ask("\nInstall TLDR code analysis tool?", default=True): + # Check if tldr is already installed (llm-tldr, not tldr-pages) + tldr_path = Path.home() / ".local" / "bin" / "tldr" + tldr_check = shutil.which("tldr") + is_llm_tldr = False + if tldr_check: + verify_result = subprocess.run( + [tldr_check, "--help"], + capture_output=True, + text=True, + timeout=10, + ) + is_llm_tldr = any(cmd in verify_result.stdout for cmd in ["tree", "structure", "daemon"]) + + if is_llm_tldr: + console.print(" [green]OK[/green] TLDR is already installed") + elif Confirm.ask("\nInstall TLDR code analysis tool?", default=True): console.print(" Installing TLDR...") import subprocess From c4309c2670ebd483ba4d010d9fda913efa16f483 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:40:42 +0700 Subject: [PATCH 25/33] fix: simplify pre-install checks to use shutil.which only - qlty: just check shutil.which('qlty') - tldr: check shutil.which and verify it's llm-tldr - ast-grep: just check shutil.which('ast-grep') --- opc/scripts/setup/wizard.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 555d8146..66248c93 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1054,11 +1054,8 @@ async def run_setup_wizard() -> None: console.print("") console.print(" [dim]Free and open source - no API key needed.[/dim]") - # Check if qlty is already installed (standalone binary, not uv tool) - qlty_path = Path.home() / ".qlty" / "bin" / "qlty" - if qlty_path.exists(): - console.print(" [green]OK[/green] qlty is already installed") - elif shutil.which("qlty"): + # Check if qlty is already installed + if shutil.which("qlty"): console.print(" [green]OK[/green] qlty is already installed") elif Confirm.ask("\nInstall qlty code quality tool?", default=True): console.print(" Installing qlty via curl...") @@ -1145,9 +1142,8 @@ async def run_setup_wizard() -> None: console.print(" [dim]Note: First semantic search downloads ~1.3GB embedding model.[/dim]") # Check if tldr is already installed (llm-tldr, not tldr-pages) - tldr_path = Path.home() / ".local" / "bin" / "tldr" - tldr_check = shutil.which("tldr") is_llm_tldr = False + tldr_check = shutil.which("tldr") if tldr_check: verify_result = subprocess.run( [tldr_check, "--help"], From efa919fbb049156f34bacf0908cf65b2a939deb8 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:48:18 +0700 Subject: [PATCH 26/33] fix: remove unreliable embedding model pre-download Model download via subprocess doesn't work because tldr module is installed in uv tool environment, not system Python. User should run 'tldr semantic index .' manually after setup. --- opc/scripts/setup/wizard.py | 37 +++++-------------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 66248c93..8d711dca 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1263,38 +1263,11 @@ async def run_setup_wizard() -> None: f" [green]OK[/green] Semantic search enabled (threshold: {threshold})" ) - # Offer to pre-download embedding model - # Note: We only download the model here, not index any directory. - # Indexing happens per-project when user runs `tldr semantic index .` - if Confirm.ask("\n Pre-download embedding model now?", default=False): - console.print(f" Downloading {model} embedding model...") - try: - # Just load the model to trigger download (no indexing) - download_result = subprocess.run( - [ - sys.executable, - "-c", - f"from tldr.semantic import get_model; get_model('{model}')", - ], - capture_output=True, - text=True, - timeout=timeout, - env={**os.environ, "TLDR_AUTO_DOWNLOAD": "1"}, - ) - if download_result.returncode == 0: - console.print(" [green]OK[/green] Embedding model downloaded") - else: - console.print(" [yellow]WARN[/yellow] Download had issues") - if download_result.stderr: - console.print(f" {download_result.stderr[:200]}") - except subprocess.TimeoutExpired: - console.print(" [yellow]WARN[/yellow] Download timed out") - except Exception as e: - console.print(f" [yellow]WARN[/yellow] {e}") - else: - console.print( - " [dim]Model downloads on first use of: tldr semantic index .[/dim]" - ) + # Note: Model download happens automatically on first use + # User can trigger manually with: tldr semantic index . + console.print( + " [dim]Embedding model downloads on first use: tldr semantic index .[/dim]" + ) else: console.print(" Semantic search disabled") console.print(" [dim]Enable later in .claude/settings.json[/dim]") From c51103e4d5e0d1134325d854a3312466a4d2d9bd Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:58:08 +0700 Subject: [PATCH 27/33] fix: use uv run for tldr model download Instead of sys.executable + module import, use 'uv run tldr semantic download-model' which works regardless of where tldr is installed. --- opc/scripts/setup/wizard.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 8d711dca..93f3df16 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1263,11 +1263,38 @@ async def run_setup_wizard() -> None: f" [green]OK[/green] Semantic search enabled (threshold: {threshold})" ) - # Note: Model download happens automatically on first use - # User can trigger manually with: tldr semantic index . - console.print( - " [dim]Embedding model downloads on first use: tldr semantic index .[/dim]" - ) + # Offer to pre-download embedding model + if Confirm.ask("\n Pre-download embedding model now?", default=False): + console.print(f" Downloading {model} embedding model...") + try: + download_result = subprocess.run( + [ + "uv", + "run", + "tldr", + "semantic", + "download-model", + "--model", + model, + ], + capture_output=True, + text=True, + timeout=300, + ) + if download_result.returncode == 0: + console.print(" [green]OK[/green] Embedding model downloaded") + else: + console.print(" [yellow]WARN[/yellow] Download had issues") + if download_result.stderr: + console.print(f" {download_result.stderr[:200]}") + except subprocess.TimeoutExpired: + console.print(" [yellow]WARN[/yellow] Download timed out") + except Exception as e: + console.print(f" [yellow]WARN[/yellow] {e}") + else: + console.print( + " [dim]Model downloads on first use of: tldr semantic index .[/dim]" + ) else: console.print(" Semantic search disabled") console.print(" [dim]Enable later in .claude/settings.json[/dim]") From 893919550ff39f95b098b1519ae5adaf3e81e07e Mon Sep 17 00:00:00 2001 From: pwnholic Date: Fri, 3 Apr 2026 23:59:00 +0700 Subject: [PATCH 28/33] fix: use shutil.which for tldr executable in model download --- opc/scripts/setup/wizard.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 93f3df16..cd4a64f8 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1267,16 +1267,9 @@ async def run_setup_wizard() -> None: if Confirm.ask("\n Pre-download embedding model now?", default=False): console.print(f" Downloading {model} embedding model...") try: + tldr_exe = shutil.which("tldr") download_result = subprocess.run( - [ - "uv", - "run", - "tldr", - "semantic", - "download-model", - "--model", - model, - ], + [tldr_exe, "semantic", "download-model", "--model", model], capture_output=True, text=True, timeout=300, @@ -1291,6 +1284,20 @@ async def run_setup_wizard() -> None: console.print(" [yellow]WARN[/yellow] Download timed out") except Exception as e: console.print(f" [yellow]WARN[/yellow] {e}") + else: + console.print( + " [dim]Model downloads on first use of: tldr semantic index .[/dim]" + ) + if download_result.returncode == 0: + console.print(" [green]OK[/green] Embedding model downloaded") + else: + console.print(" [yellow]WARN[/yellow] Download had issues") + if download_result.stderr: + console.print(f" {download_result.stderr[:200]}") + except subprocess.TimeoutExpired: + console.print(" [yellow]WARN[/yellow] Download timed out") + except Exception as e: + console.print(f" [yellow]WARN[/yellow] {e}") else: console.print( " [dim]Model downloads on first use of: tldr semantic index .[/dim]" From d985fc0d497fe471628b1c8e484b4365d4ee0e0a Mon Sep 17 00:00:00 2001 From: pwnholic Date: Sat, 4 Apr 2026 00:02:14 +0700 Subject: [PATCH 29/33] fix: use 'tldr semantic index' instead of 'tldr semantic download-model' There is no 'download-model' command - model downloads automatically when running 'tldr semantic index'. Using correct command now. --- opc/scripts/setup/wizard.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index cd4a64f8..3db571c7 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1269,7 +1269,7 @@ async def run_setup_wizard() -> None: try: tldr_exe = shutil.which("tldr") download_result = subprocess.run( - [tldr_exe, "semantic", "download-model", "--model", model], + [tldr_exe, "semantic", "index", "--model", model], capture_output=True, text=True, timeout=300, @@ -1284,20 +1284,6 @@ async def run_setup_wizard() -> None: console.print(" [yellow]WARN[/yellow] Download timed out") except Exception as e: console.print(f" [yellow]WARN[/yellow] {e}") - else: - console.print( - " [dim]Model downloads on first use of: tldr semantic index .[/dim]" - ) - if download_result.returncode == 0: - console.print(" [green]OK[/green] Embedding model downloaded") - else: - console.print(" [yellow]WARN[/yellow] Download had issues") - if download_result.stderr: - console.print(f" {download_result.stderr[:200]}") - except subprocess.TimeoutExpired: - console.print(" [yellow]WARN[/yellow] Download timed out") - except Exception as e: - console.print(f" [yellow]WARN[/yellow] {e}") else: console.print( " [dim]Model downloads on first use of: tldr semantic index .[/dim]" From bf6a43ca275d7071ba000acf0f5d12439ec0536a Mon Sep 17 00:00:00 2001 From: pwnholic Date: Sat, 4 Apr 2026 00:05:46 +0700 Subject: [PATCH 30/33] fix: use original pre-download code with sys.executable --- opc/scripts/setup/wizard.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 3db571c7..66248c93 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1264,15 +1264,22 @@ async def run_setup_wizard() -> None: ) # Offer to pre-download embedding model + # Note: We only download the model here, not index any directory. + # Indexing happens per-project when user runs `tldr semantic index .` if Confirm.ask("\n Pre-download embedding model now?", default=False): console.print(f" Downloading {model} embedding model...") try: - tldr_exe = shutil.which("tldr") + # Just load the model to trigger download (no indexing) download_result = subprocess.run( - [tldr_exe, "semantic", "index", "--model", model], + [ + sys.executable, + "-c", + f"from tldr.semantic import get_model; get_model('{model}')", + ], capture_output=True, text=True, - timeout=300, + timeout=timeout, + env={**os.environ, "TLDR_AUTO_DOWNLOAD": "1"}, ) if download_result.returncode == 0: console.print(" [green]OK[/green] Embedding model downloaded") From 5543a73fa7a6f8bf37bb3b901f791560d5cb4009 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Sat, 4 Apr 2026 00:07:17 +0700 Subject: [PATCH 31/33] fix: use uv run python for model download Using sys.executable fails because tldr module is in uv tool environment, not system Python. Using 'uv run python' instead. --- opc/scripts/setup/wizard.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opc/scripts/setup/wizard.py b/opc/scripts/setup/wizard.py index 66248c93..c4395ed6 100644 --- a/opc/scripts/setup/wizard.py +++ b/opc/scripts/setup/wizard.py @@ -1272,7 +1272,9 @@ async def run_setup_wizard() -> None: # Just load the model to trigger download (no indexing) download_result = subprocess.run( [ - sys.executable, + "uv", + "run", + "python", "-c", f"from tldr.semantic import get_model; get_model('{model}')", ], From ba65f3912039ce94b726853359646145d5afb501 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Sat, 4 Apr 2026 00:27:11 +0700 Subject: [PATCH 32/33] docs: update status.py with comprehensive description Add detailed docstring explaining: - Output format with examples - Color coding scheme - All components (tokens, context%, branch, S/U/A counts, goal, now) - Temporary file locations --- .claude/scripts/status.py | 109 +++++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/.claude/scripts/status.py b/.claude/scripts/status.py index 04ee7f61..1a09919b 100644 --- a/.claude/scripts/status.py +++ b/.claude/scripts/status.py @@ -1,8 +1,32 @@ #!/usr/bin/env python3 """Cross-platform status line for Claude Code. -Shows: 145K 72% | main U:6 | Goal → Current focus -Critical: ⚠ 160K 80% | main U:6 | Current focus +Output Format: + {tokens} {context}% | {branch} S:{staged} U:{unstaged} A:{added} | {goal} → {now} + +Examples: + 78K 39% | main A:4 | Fix wizard → Update timeout + 145K 72% | main S:2 U:1 | Refactor OPC → Add validation + ⚠ 160K 80% | dev U:6 | Critical bug fix + +Color Coding: + Green (ctx < 60%): Normal operation + Yellow (ctx 60-79%): Warning, consider cleanup + Red (ctx ≥ 80%): Critical, show only now focus + +Components: + tokens - Total tokens used (input + cache_read + cache_creation + overhead) + context% - Percentage of context window consumed + branch - Git branch name (max 12 chars) + S:N - Staged files (git add done) + U:N - Unstaged files (changed but not staged) + A:N - Untracked files (new files not git-added) + goal - Objective from handoff file (max 25 chars) + now - Current focus from handoff (max 30 chars) + +Temporary Files: + ~/.tmp/claude-context-pct-{session_id}.txt - Context % for other hooks + ~/.tmp/claude-session-stats-{session_id}.json - Full stats for /tldr-stats skill Replaces status.sh for Windows compatibility. """ @@ -89,11 +113,14 @@ def log_context_drop(session_id: str, prev_pct: int, curr_pct: int) -> None: Logs to ~/.claude/autocompact.log (local, not pushed to repo). """ from datetime import datetime + log_file = Path.home() / ".claude" / "autocompact.log" try: with open(log_file, "a") as f: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - f.write(f"{timestamp} | session:{session_id} | {prev_pct}% → {curr_pct}% (drop: {prev_pct - curr_pct}%)\n") + f.write( + f"{timestamp} | session:{session_id} | {prev_pct}% → {curr_pct}% (drop: {prev_pct - curr_pct}%)\n" + ) except OSError: pass @@ -180,15 +207,27 @@ def get_git_info(cwd: Path) -> str: # Check if git repo result = subprocess.run( ["git", "-C", str(cwd), "rev-parse", "--git-dir"], - capture_output=True, text=True, timeout=5 + capture_output=True, + text=True, + timeout=5, ) if result.returncode != 0: return "" # Get branch name result = subprocess.run( - ["git", "-C", str(cwd), "--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD"], - capture_output=True, text=True, timeout=5 + [ + "git", + "-C", + str(cwd), + "--no-optional-locks", + "rev-parse", + "--abbrev-ref", + "HEAD", + ], + capture_output=True, + text=True, + timeout=5, ) branch = result.stdout.strip() if result.returncode == 0 else "" if len(branch) > 12: @@ -196,22 +235,46 @@ def get_git_info(cwd: Path) -> str: # Get staged count result = subprocess.run( - ["git", "-C", str(cwd), "--no-optional-locks", "diff", "--cached", "--name-only"], - capture_output=True, text=True, timeout=5 + [ + "git", + "-C", + str(cwd), + "--no-optional-locks", + "diff", + "--cached", + "--name-only", + ], + capture_output=True, + text=True, + timeout=5, ) staged = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 # Get unstaged count result = subprocess.run( ["git", "-C", str(cwd), "--no-optional-locks", "diff", "--name-only"], - capture_output=True, text=True, timeout=5 + capture_output=True, + text=True, + timeout=5, + ) + unstaged = ( + len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 ) - unstaged = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 # Get untracked count result = subprocess.run( - ["git", "-C", str(cwd), "--no-optional-locks", "ls-files", "--others", "--exclude-standard"], - capture_output=True, text=True, timeout=5 + [ + "git", + "-C", + str(cwd), + "--no-optional-locks", + "ls-files", + "--others", + "--exclude-standard", + ], + capture_output=True, + text=True, + timeout=5, ) added = len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0 @@ -239,8 +302,8 @@ def parse_filename_timestamp(path: Path) -> str: Returns '0000-00-00_00-00' if no timestamp found (sorts oldest). """ - match = re.search(r'(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})', path.name) - return match.group(1) if match else '0000-00-00_00-00' + match = re.search(r"(\d{4}-\d{2}-\d{2}_\d{2}-\d{2})", path.name) + return match.group(1) if match else "0000-00-00_00-00" def find_latest_handoff(project_dir: Path) -> Path | None: @@ -267,7 +330,7 @@ def extract_yaml_field(content: str, field: str) -> str: pattern = rf"^{field}:\s*(.+?)$" match = re.search(pattern, content, re.MULTILINE) if match: - return match.group(1).strip().strip('"\'') + return match.group(1).strip().strip("\"'") return "" @@ -300,14 +363,19 @@ def get_continuity_info(project_dir: Path) -> tuple[str, str]: # Fallback for now: Action Items or Next Steps section if not now_focus: - match = re.search(r"^## (?:Action Items|Next Steps)\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", - content, re.MULTILINE) + match = re.search( + r"^## (?:Action Items|Next Steps)\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", + content, + re.MULTILINE, + ) if match: now_focus = match.group(2).strip() # Try P0 section if not now_focus: - match = re.search(r"^### P0\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", content, re.MULTILINE) + match = re.search( + r"^### P0\s*\n(?:.*\n)*?^(\d+\.)\s*(.+?)$", content, re.MULTILINE + ) if match: now_focus = match.group(2).strip() @@ -346,8 +414,9 @@ def get_continuity_info(project_dir: Path) -> tuple[str, str]: return goal, now_focus -def build_output(context_pct: int, token_display: str, git_info: str, - goal: str, now_focus: str) -> str: +def build_output( + context_pct: int, token_display: str, git_info: str, goal: str, now_focus: str +) -> str: """Build the final colored output string.""" # Build continuity string if goal and now_focus: From 3194843c783086658d4abef780a488afb42707d8 Mon Sep 17 00:00:00 2001 From: pwnholic Date: Sat, 4 Apr 2026 00:32:22 +0700 Subject: [PATCH 33/33] fix: update status line format with consistent labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONTEXT: {tokens} {context}% | GIT: {branch} S:{staged} U:{unstaged} A:{added} | GOAL: {goal} → NOW: {now} --- .claude/scripts/status.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.claude/scripts/status.py b/.claude/scripts/status.py index 1a09919b..ce794dd9 100644 --- a/.claude/scripts/status.py +++ b/.claude/scripts/status.py @@ -2,12 +2,12 @@ """Cross-platform status line for Claude Code. Output Format: - {tokens} {context}% | {branch} S:{staged} U:{unstaged} A:{added} | {goal} → {now} + CONTEXT: {tokens} {context}% | GIT: {branch} S:{staged} U:{unstaged} A:{added} | GOAL: {goal} → NOW: {now} Examples: - 78K 39% | main A:4 | Fix wizard → Update timeout - 145K 72% | main S:2 U:1 | Refactor OPC → Add validation - ⚠ 160K 80% | dev U:6 | Critical bug fix + CONTEXT: 78K 39% | GIT: main A:4 | GOAL: Fix wizard → NOW: Update timeout + CONTEXT: 145K 72% | GIT: main S:2 U:1 | GOAL: Refactor OPC → NOW: Add validation + ⚠ CONTEXT: 160K 80% | GIT: dev U:6 | NOW: Critical bug fix Color Coding: Green (ctx < 60%): Normal operation @@ -420,37 +420,37 @@ def build_output( """Build the final colored output string.""" # Build continuity string if goal and now_focus: - continuity = f"{goal} → {now_focus}" + continuity = f"GOAL: {goal} → NOW: {now_focus}" elif now_focus: - continuity = now_focus + continuity = f"NOW: {now_focus}" elif goal: - continuity = goal + continuity = f"GOAL: {goal}" else: continuity = "" # Color based on context usage if context_pct >= 80: # CRITICAL - Red warning - ctx_display = f"\033[31m⚠ {token_display} {context_pct}%\033[0m" + ctx_display = f"\033[31m⚠ CONTEXT: {token_display} {context_pct}%\033[0m" parts = [ctx_display] if git_info: - parts.append(git_info) + parts.append(f"GIT: {git_info}") if now_focus: # Only show now_focus when critical - parts.append(now_focus) + parts.append(continuity) elif context_pct >= 60: # WARNING - Yellow - ctx_display = f"\033[33m{token_display} {context_pct}%\033[0m" + ctx_display = f"\033[33mCONTEXT: {token_display} {context_pct}%\033[0m" parts = [ctx_display] if git_info: - parts.append(git_info) + parts.append(f"GIT: {git_info}") if continuity: parts.append(continuity) else: # NORMAL - Green - ctx_display = f"\033[32m{token_display} {context_pct}%\033[0m" + ctx_display = f"\033[32mCONTEXT: {token_display} {context_pct}%\033[0m" parts = [ctx_display] if git_info: - parts.append(git_info) + parts.append(f"GIT: {git_info}") if continuity: parts.append(continuity)