diff --git a/.github/workflows/create-pr-next.yml b/.github/workflows/create-pr-next.yml new file mode 100644 index 00000000..d3f688e7 --- /dev/null +++ b/.github/workflows/create-pr-next.yml @@ -0,0 +1,55 @@ +name: Auto open PR for next branch +on: + push: + branches: + - next +permissions: + contents: read + pull-requests: write +jobs: + open-pull-request: + runs-on: ubuntu-latest + steps: + - name: Create pull request if none exists + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo + const head = context.ref.replace('refs/heads/', '') + const base = 'main' + + // Verify this is the next branch + if (head !== 'next') { + core.info(`Branch is '${head}', not 'next'. Skipping PR creation.`) + return + } + + // Check if a PR already exists from next to main + const { data: existingPRs } = await github.rest.pulls.list({ + owner: owner, + repo: repo, + state: 'open', + base: base, + head: `${owner}:${head}` + }) + + if (existingPRs.length > 0) { + core.info(`Open pull request already exists (#${existingPRs[0].number}).`) + return + } + + // Create the PR + const title = `Auto PR: ${head} β†’ ${base}` + const body = 'Automatically opened pull request for the `next` branch.' + + const { data: newPR } = await github.rest.pulls.create({ + owner: owner, + repo: repo, + title: title, + head: head, + base: base, + body: body + }) + + core.info(`Created pull request #${newPR.number}.`) diff --git a/conf/llm/docs/coding-rules.md b/conf/llm/docs/coding-rules.md index cf17ee4f..7d4c9f02 100644 --- a/conf/llm/docs/coding-rules.md +++ b/conf/llm/docs/coding-rules.md @@ -29,3 +29,5 @@ - **User Preferences**: - VCS: `jj` - Search: `fd` and `rg` +- **git**: + - Only run git commit commands after user confirm the changes is ok. diff --git a/nix/hm/ai/claude/agents/oracle.md b/nix/hm/ai/claude/agents/oracle.md index 038b2db9..417c7345 100644 --- a/nix/hm/ai/claude/agents/oracle.md +++ b/nix/hm/ai/claude/agents/oracle.md @@ -36,18 +36,13 @@ You are the Oracle - an expert AI advisor for complex technical decisions. - Ensuring every abstraction justifies - Complexity is only introduced when it solves real problems - ## Tool usage -- **codex** (mcp__codex__codex): Use for deep reasoning on complex problems - - Profile: "claude_fast" (default) or "claude" (very complex cases) - - Continue: mcp__codex__codex-reply - - NOT for simple tasks or command execution - - NOT for codebase analysis, files searching, or basic grep/read tasks - **brightdata**: Latest web context (versions, best practices, docs) -- **context7**: Official library documentation (resolve-library-id first, then get-library-docs) - You do not have Write, Bash tool usage, if you need to run such commands, you must output your requirements and finish - If you need more context, output your requirements and finish +- kg: Search in our knowledge graph for similar issues, notes +- github: Search github issues when solving issues that related to open source projects ## Output format (required) diff --git a/nix/hm/litellm/bender-muffin.nix b/nix/hm/litellm/bender-muffin.nix index efd0f512..1d59a6df 100644 --- a/nix/hm/litellm/bender-muffin.nix +++ b/nix/hm/litellm/bender-muffin.nix @@ -6,23 +6,23 @@ }: [ + { + model_name = "bender-muffin"; + litellm_params = { + model = "github_copilot/claude-sonnet-4.5"; + extra_headers = copilotHeaders; + max_tokens = modelTokenMax "claude-sonnet-4.5"; + rpm = 1; + }; + } { model_name = "bender-muffin"; litellm_params = { model = "github_copilot/claude-haiku-4.5"; extra_headers = copilotHeaders; max_tokens = modelTokenMax "claude-haiku-4.5"; - rpm = 1000; }; } - # { - # model_name = "bender-muffin"; - # litellm_params = { - # model = "github_copilot/oswe-vscode-prime"; - # extra_headers = copilotHeaders; - # max_tokens = modelTokenMax "grok-code-fast-1"; - # }; - # } { model_name = "bender-muffin"; litellm_params = { @@ -30,26 +30,7 @@ api_key = pkgs.nix-priv.keys.alimodel.apiKey; api_base = "https://dashscope.aliyuncs.com/compatible-mode/v1"; max_tokens = 65536; - rpm = 1000; - }; - } - { - model_name = "bender-muffin"; - litellm_params = { - model = "openai/glm-4.6"; - api_base = "https://open.bigmodel.cn/api/coding/paas/v4"; - api_key = pkgs.nix-priv.keys.zai.apiKey; - max_tokens = 128000; - rpm = 1000; - }; - } - { - model_name = "bender-muffin"; - litellm_params = { - model = "openrouter/openai/gpt-5.1-codex-mini"; - api_key = "os.environ/OPENROUTER_API_KEY"; - max_tokens = 100000; - rpm = 50; + rpm = 3; }; } { @@ -61,15 +42,4 @@ max_tokens = 128000; }; } - ## have issues, kimi - # { - # model_name = "bender-muffin"; - # litellm_params = { - # model = "moonshot/kimi-k2-0905-preview"; - # api_base = "https://api.moonshot.cn/v1"; - # api_key = "${pkgs.nix-priv.keys.moonshot.apiKey}"; - # max_tokens = 262144; - # max_output_tokens = 262144; - # }; - # } ] diff --git a/nix/hm/litellm/frontier-muffin.nix b/nix/hm/litellm/frontier-muffin.nix index 3e73a592..05feab67 100644 --- a/nix/hm/litellm/frontier-muffin.nix +++ b/nix/hm/litellm/frontier-muffin.nix @@ -12,7 +12,12 @@ model = "github_copilot/claude-sonnet-4.5"; extra_headers = copilotHeaders; max_tokens = modelTokenMax "claude-sonnet-4.5"; - max_output_tokens = modelTokenMax "claude-sonnet-4.5"; + cache_control_injection_points = [ + { + location = "message"; + role = "user"; + } + ]; }; } { @@ -24,4 +29,19 @@ max_output_tokens = modelTokenMax "gpt-5"; }; } + { + model_name = "frontier-muffin"; + litellm_params = { + model = "openrouter/anthropic/claude-3.7-sonnet:thinking"; + api_key = "os.environ/OPENROUTER_API_KEY"; + cache_control_injection_points = [ + { + location = "message"; + role = "user"; + } + ]; + max_tokens = 64000; + rpm = 1; + }; + } ] diff --git a/scripts/claude-statusline.py b/scripts/claude-statusline.py index 4ac25c99..b6d78593 100755 --- a/scripts/claude-statusline.py +++ b/scripts/claude-statusline.py @@ -4,6 +4,19 @@ Displays contextual information about the current session """ +# Customize these icons to your preference +# Examples: +# - Use emojis: "πŸ“‚", "⏳", "πŸ’°", etc. +# - Use nerdfonts: "", "ξ™”", "", etc. +# - Use simple symbols: "πŸ“", "⏱️", "$", etc. +ICON_DIR = "σ°‹œ" +ICON_BRANCH = "󰊒" +ICON_SESSION = "σ°“Ή" +ICON_CONTEXT = "οˆ€" +ICON_COST = "" +ICON_LINES = "" +ICON_DURATION = "󱑆" + import json import sys import os @@ -19,10 +32,7 @@ def get_git_branch(cwd): # Check if we're in a git repo result = subprocess.run( - ["git", "rev-parse", "--git-dir"], - capture_output=True, - text=True, - timeout=1 + ["git", "rev-parse", "--git-dir"], capture_output=True, text=True, timeout=1 ) if result.returncode != 0: @@ -34,13 +44,13 @@ def get_git_branch(cwd): ["git", "branch", "--show-current"], capture_output=True, text=True, - timeout=1 + timeout=1, ) os.chdir(original_cwd) branch = result.stdout.strip() - return f" | 🌿 {branch}" if branch else "" + return f" | {ICON_BRANCH} {branch}" if branch else "" except Exception: return "" @@ -51,8 +61,8 @@ def get_token_metrics(transcript_path): if not os.path.exists(transcript_path): return None - with open(transcript_path, 'r') as f: - lines = f.read().strip().split('\n') + with open(transcript_path, "r") as f: + lines = f.read().strip().split("\n") input_tokens = 0 output_tokens = 0 @@ -65,39 +75,44 @@ def get_token_metrics(transcript_path): for line in lines: try: data = json.loads(line) - if data.get('message', {}).get('usage'): - usage = data['message']['usage'] - input_tokens += usage.get('input_tokens', 0) - output_tokens += usage.get('output_tokens', 0) - cached_tokens += usage.get('cache_read_input_tokens', 0) - cached_tokens += usage.get('cache_creation_input_tokens', 0) + if data.get("message", {}).get("usage"): + usage = data["message"]["usage"] + input_tokens += usage.get("input_tokens", 0) + output_tokens += usage.get("output_tokens", 0) + cached_tokens += usage.get("cache_read_input_tokens", 0) + cached_tokens += usage.get("cache_creation_input_tokens", 0) # Track most recent main chain entry - if not data.get('isSidechain', False) and data.get('timestamp'): - timestamp = data['timestamp'] - if most_recent_timestamp is None or timestamp > most_recent_timestamp: + if not data.get("isSidechain", False) and data.get("timestamp"): + timestamp = data["timestamp"] + if ( + most_recent_timestamp is None + or timestamp > most_recent_timestamp + ): most_recent_timestamp = timestamp most_recent_main_chain_entry = data except: continue # Calculate context length from most recent main chain message - if most_recent_main_chain_entry and most_recent_main_chain_entry.get('message', {}).get('usage'): - usage = most_recent_main_chain_entry['message']['usage'] + if most_recent_main_chain_entry and most_recent_main_chain_entry.get( + "message", {} + ).get("usage"): + usage = most_recent_main_chain_entry["message"]["usage"] context_length = ( - usage.get('input_tokens', 0) + - usage.get('cache_read_input_tokens', 0) + - usage.get('cache_creation_input_tokens', 0) + usage.get("input_tokens", 0) + + usage.get("cache_read_input_tokens", 0) + + usage.get("cache_creation_input_tokens", 0) ) total_tokens = input_tokens + output_tokens + cached_tokens return { - 'input_tokens': input_tokens, - 'output_tokens': output_tokens, - 'cached_tokens': cached_tokens, - 'total_tokens': total_tokens, - 'context_length': context_length + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "cached_tokens": cached_tokens, + "total_tokens": total_tokens, + "context_length": context_length, } except: return None @@ -116,7 +131,7 @@ def format_cost(cost_usd): """Format cost in USD""" if cost_usd is None or cost_usd == 0: return "" - return f" | πŸ’° ${cost_usd:.4f}" + return f" | {ICON_COST} ${cost_usd:.4f}" def format_lines(added, removed): @@ -130,7 +145,7 @@ def format_lines(added, removed): if removed and removed > 0: parts.append(f"-{removed}") - return f" | πŸ“ {'/'.join(parts)}" if parts else "" + return f" | {ICON_LINES} {'/'.join(parts)}" if parts else "" def format_duration(duration_ms): @@ -140,11 +155,11 @@ def format_duration(duration_ms): seconds = duration_ms / 1000 if seconds < 60: - return f" | ⏱️ {seconds:.1f}s" + return f" | {ICON_DURATION} {seconds:.1f}s" else: minutes = int(seconds // 60) secs = int(seconds % 60) - return f" | ⏱️ {minutes}m {secs}s" + return f" | {ICON_DURATION} {minutes}m {secs}s" def format_context_stats(token_metrics): @@ -152,7 +167,7 @@ def format_context_stats(token_metrics): if not token_metrics: return "" - context_length = token_metrics.get('context_length', 0) + context_length = token_metrics.get("context_length", 0) if context_length == 0: return "" @@ -160,7 +175,7 @@ def format_context_stats(token_metrics): max_tokens = 200000 percentage = min(100, (context_length / max_tokens) * 100) - return f" | πŸ“Š {format_tokens(context_length)} ({percentage:.1f}%)" + return f" | {ICON_CONTEXT} {format_tokens(context_length)} ({percentage:.1f}%)" def format_session_id(session_id): @@ -168,7 +183,7 @@ def format_session_id(session_id): if not session_id: return "" # Show first 8 characters of session ID - return f" | πŸ”– {session_id[:8]}" + return f" | {ICON_SESSION} {session_id[:8]}" def main(): @@ -190,8 +205,7 @@ def main(): # Optional: Get lines changed lines_info = format_lines( - cost.get("total_lines_added"), - cost.get("total_lines_removed") + cost.get("total_lines_added"), cost.get("total_lines_removed") ) # Optional: Get context states @@ -207,7 +221,7 @@ def main(): context_stats = format_context_stats(token_metrics) # Build status line - status = f"[{model}] πŸ“ {dir_name}{git_branch}{session_info}{context_stats}{cost_info}{lines_info}{duration_info}" + status = f"[{model}] ο““ {dir_name}{git_branch}{session_info}{context_stats}{cost_info}{lines_info}{duration_info}" print(status)