diff --git a/README-old.md b/README-old.md deleted file mode 100644 index 0b090077..00000000 --- a/README-old.md +++ /dev/null @@ -1,210 +0,0 @@ -# Claw Code - -

- ultraworkers/claw-code - · - Usage - · - Rust workspace - · - Parity - · - Roadmap - · - UltraWorkers Discord -

- -

- - - - - Star history for ultraworkers/claw-code - - -

- -

- Claw Code -

- -Claw Code is the public Rust implementation of the `claw` CLI agent harness. -The canonical implementation lives in [`rust/`](./rust), and the current source of truth for this repository is **ultraworkers/claw-code**. - -> [!IMPORTANT] -> Start with [`USAGE.md`](./USAGE.md) for build, auth, CLI, session, and parity-harness workflows. Make `claw doctor` your first health check after building, use [`rust/README.md`](./rust/README.md) for crate-level details, read [`PARITY.md`](./PARITY.md) for the current Rust-port checkpoint, and see [`docs/container.md`](./docs/container.md) for the container-first workflow. -> -> **ACP / Zed status:** `claw-code` does not ship an ACP/Zed daemon entrypoint yet. Run `claw acp` (or `claw --acp`) for the current status instead of guessing from source layout; `claw acp serve` is currently a discoverability alias only, and real ACP support remains tracked separately in `ROADMAP.md`. - -## Current repository shape - -- **`rust/`** — canonical Rust workspace and the `claw` CLI binary -- **`USAGE.md`** — task-oriented usage guide for the current product surface -- **`PARITY.md`** — Rust-port parity status and migration notes -- **`ROADMAP.md`** — active roadmap and cleanup backlog -- **`PHILOSOPHY.md`** — project intent and system-design framing -- **`src/` + `tests/`** — companion Python/reference workspace and audit helpers; not the primary runtime surface - -## Quick start - -> [!NOTE] -> [!WARNING] -> **`cargo install claw-code` installs the wrong thing.** The `claw-code` crate on crates.io is a deprecated stub that places `claw-code-deprecated.exe` — not `claw`. Running it only prints `"claw-code has been renamed to agent-code"`. **Do not use `cargo install claw-code`.** Either build from source (this repo) or install the upstream binary: -> ```bash -> cargo install agent-code # upstream binary — installs 'agent.exe' (Windows) / 'agent' (Unix), NOT 'agent-code' -> ``` -> This repo (`ultraworkers/claw-code`) is **build-from-source only** — follow the steps below. - -```bash -# 1. Clone and build -git clone https://github.com/ultraworkers/claw-code -cd claw-code/rust -cargo build --workspace - -# 2. Set your API key (Anthropic API key — not a Claude subscription) -export ANTHROPIC_API_KEY="sk-ant-..." - -# 3. Verify everything is wired correctly -./target/debug/claw doctor - -# 4. Run a prompt -./target/debug/claw prompt "say hello" -``` - -> [!NOTE] -> **Windows (PowerShell):** the binary is `claw.exe`, not `claw`. Use `.\target\debug\claw.exe` or run `cargo run -- prompt "say hello"` to skip the path lookup. - -### Windows setup - -**PowerShell is a supported Windows path.** Use whichever shell works for you. The common onboarding issues on Windows are: - -1. **Install Rust first** — download from and run the installer. Close and reopen your terminal when it finishes. -2. **Verify Rust is on PATH:** - ```powershell - cargo --version - ``` - If this fails, reopen your terminal or run the PATH setup from the Rust installer output, then retry. -3. **Clone and build** (works in PowerShell, Git Bash, or WSL): - ```powershell - git clone https://github.com/ultraworkers/claw-code - cd claw-code/rust - cargo build --workspace - ``` -4. **Run** (PowerShell — note `.exe` and backslash): - ```powershell - $env:ANTHROPIC_API_KEY = "sk-ant-..." - .\target\debug\claw.exe prompt "say hello" - ``` - -**Git Bash / WSL** are optional alternatives, not requirements. If you prefer bash-style paths (`/c/Users/you/...` instead of `C:\Users\you\...`), Git Bash (ships with Git for Windows) works well. In Git Bash, the `MINGW64` prompt is expected and normal — not a broken install. - -## Post-build: locate the binary and verify - -After running `cargo build --workspace`, the `claw` binary is built but **not** automatically installed to your system. Here's where to find it and how to verify the build succeeded. - -### Binary location - -After `cargo build --workspace` in `claw-code/rust/`: - -**Debug build (default, faster compile):** -- **macOS/Linux:** `rust/target/debug/claw` -- **Windows:** `rust/target/debug/claw.exe` - -**Release build (optimized, slower compile):** -- **macOS/Linux:** `rust/target/release/claw` -- **Windows:** `rust/target/release/claw.exe` - -If you ran `cargo build` without `--release`, the binary is in the `debug/` folder. - -### Verify the build succeeded - -Test the binary directly using its path: - -```bash -# macOS/Linux (debug build) -./rust/target/debug/claw --help -./rust/target/debug/claw doctor - -# Windows PowerShell (debug build) -.\rust\target\debug\claw.exe --help -.\rust\target\debug\claw.exe doctor -``` - -If these commands succeed, the build is working. `claw doctor` is your first health check — it validates your API key, model access, and tool configuration. - -### Optional: Add to PATH - -If you want to run `claw` from any directory without the full path, choose one of these approaches: - -**Option 1: Symlink (macOS/Linux)** -```bash -ln -s $(pwd)/rust/target/debug/claw /usr/local/bin/claw -``` -Then reload your shell and test: -```bash -claw --help -``` - -**Option 2: Use `cargo install` (all platforms)** - -Build and install to Cargo's default location (`~/.cargo/bin/`, which is usually on PATH): -```bash -# From the claw-code/rust/ directory -cargo install --path . --force - -# Then from anywhere -claw --help -``` - -**Option 3: Update shell profile (bash/zsh)** - -Add this line to `~/.bashrc` or `~/.zshrc`: -```bash -export PATH="$(pwd)/rust/target/debug:$PATH" -``` - -Reload your shell: -```bash -source ~/.bashrc # or source ~/.zshrc -claw --help -``` - -### Troubleshooting - -- **"command not found: claw"** — The binary is in `rust/target/debug/claw`, but it's not on your PATH. Use the full path `./rust/target/debug/claw` or symlink/install as above. -- **"permission denied"** — On macOS/Linux, you may need `chmod +x rust/target/debug/claw` if the executable bit isn't set (rare). -- **Debug vs. release** — If the build is slow, you're in debug mode (default). Add `--release` to `cargo build` for faster runtime, but the build itself will take 5–10 minutes. - -> [!NOTE] -> **Auth:** claw requires an **API key** (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) — Claude subscription login is not a supported auth path. - -Run the workspace test suite after verifying the binary works: - -```bash -cd rust -cargo test --workspace -``` - -## Documentation map - -- [`USAGE.md`](./USAGE.md) — quick commands, auth, sessions, config, parity harness -- [`rust/README.md`](./rust/README.md) — crate map, CLI surface, features, workspace layout -- [`PARITY.md`](./PARITY.md) — parity status for the Rust port -- [`rust/MOCK_PARITY_HARNESS.md`](./rust/MOCK_PARITY_HARNESS.md) — deterministic mock-service harness details -- [`ROADMAP.md`](./ROADMAP.md) — active roadmap and open cleanup work -- [`PHILOSOPHY.md`](./PHILOSOPHY.md) — why the project exists and how it is operated - -## Ecosystem - -Claw Code is built in the open alongside the broader UltraWorkers toolchain: - -- [clawhip](https://github.com/Yeachan-Heo/clawhip) -- [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent) -- [oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode) -- [oh-my-codex](https://github.com/Yeachan-Heo/oh-my-codex) -- [UltraWorkers Discord](https://discord.gg/5TUQKqFWd) - -## Ownership / affiliation disclaimer - -- This repository does **not** claim ownership of the original Claude Code source material. -- This repository is **not affiliated with, endorsed by, or maintained by Anthropic**. diff --git a/add_imports.py b/add_imports.py deleted file mode 100644 index d6ad39f5..00000000 --- a/add_imports.py +++ /dev/null @@ -1,39 +0,0 @@ -import os - -files = [ - 'rust/crates/rusty-claude-cli/src/execution/client.rs', - 'rust/crates/rusty-claude-cli/src/execution/stream.rs', - 'rust/crates/rusty-claude-cli/src/execution/executor.rs' -] - -imports = """ -use crate::*; -use api::*; -use runtime::*; -use std::io::{self, Write}; -use std::sync::{Arc, Mutex}; -use std::path::Path; -""" - -for file in files: - with open(file, 'r') as f: - content = f.read() - with open(file, 'w') as f: - f.write(imports + "\n" + content) - -main_file = "rust/crates/rusty-claude-cli/src/main.rs" -with open(main_file, 'r') as f: - main_content = f.read() - -main_imports = """ -use crate::execution::client::*; -use crate::execution::stream::*; -use crate::execution::executor::*; -""" -# inject right after mod declarations -main_content = main_content.replace("pub(crate) mod ui;\n", "pub(crate) mod ui;\n" + main_imports) - -with open(main_file, 'w') as f: - f.write(main_content) - -print("Imports added.") diff --git a/anthropic_full.rs b/anthropic_full.rs deleted file mode 100644 index c307a0d1..00000000 --- a/anthropic_full.rs +++ /dev/null @@ -1,255 +0,0 @@ -pub(crate) struct AnthropicRuntimeClient { - runtime: tokio::runtime::Runtime, - client: ApiProviderClient, - session_id: String, - model: String, - enable_tools: bool, - emit_output: bool, - allowed_tools: Option, - tool_registry: GlobalToolRegistry, - progress_reporter: Option, - reasoning_effort: Option, -} -impl AnthropicRuntimeClient { - pub(crate) fn new( - session_id: &str, - model: String, - enable_tools: bool, - emit_output: bool, - allowed_tools: Option, - tool_registry: GlobalToolRegistry, - progress_reporter: Option, - ) -> Result> { - // Dispatch to the correct provider at construction time. - // `ApiProviderClient` (exposed by the api crate as - // `ProviderClient`) is an enum over Anthropic / xAI / OpenAI - // variants, where xAI and OpenAI both use the OpenAI-compat - // wire format under the hood. We consult - // `detect_provider_kind(&resolved_model)` so model-name prefix - // routing (`openai/`, `gpt-`, `grok`, `qwen/`) wins over - // env-var presence. - // - // For Anthropic we build the client directly instead of going - // through `ApiProviderClient::from_model_with_anthropic_auth` - // so we can explicitly apply `api::read_base_url()` — that - // reads `ANTHROPIC_BASE_URL` and is required for the local - // mock-server test harness - // (`crates/rusty-claude-cli/tests/compact_output.rs`) to point - // claw at its fake Anthropic endpoint. We also attach a - // session-scoped prompt cache on the Anthropic path; the - // prompt cache is Anthropic-only so non-Anthropic variants - // skip it. - let resolved_model = api::resolve_model_alias(&model); - let client = match detect_provider_kind(&resolved_model) { - ProviderKind::Anthropic => { - let auth = resolve_cli_auth_source()?; - let inner = AnthropicClient::from_auth(auth) - .with_base_url(api::read_base_url()) - .with_prompt_cache(PromptCache::new(session_id)); - ApiProviderClient::Anthropic(inner) - } - ProviderKind::Xai | ProviderKind::OpenAi => { - // The api crate's `ProviderClient::from_model_with_anthropic_auth` - // with `None` for the anthropic auth routes via - // `detect_provider_kind` and builds an - // `OpenAiCompatClient::from_env` with the matching - // `OpenAiCompatConfig` (openai / xai / dashscope). - // That reads the correct API-key env var and BASE_URL - // override internally, so this one call covers OpenAI, - // OpenRouter, xAI, DashScope, Ollama, and any other - // OpenAI-compat endpoint users configure via - // `OPENAI_BASE_URL` / `XAI_BASE_URL` / `DASHSCOPE_BASE_URL`. - ApiProviderClient::from_model_with_anthropic_auth(&resolved_model, None)? - } - }; - Ok(Self { - runtime: tokio::runtime::Runtime::new()?, - client, - session_id: session_id.to_string(), - model, - enable_tools, - emit_output, - allowed_tools, - tool_registry, - progress_reporter, - reasoning_effort: None, - }) - } - - pub(crate) fn set_reasoning_effort(&mut self, effort: Option) { - self.reasoning_effort = effort; - } -} -impl AnthropicRuntimeClient { - /// Consume a single streaming response, optionally applying a stall - /// timeout on the first event for post-tool continuations. - #[allow(clippy::too_many_lines)] - pub(crate) async fn consume_stream( - &self, - message_request: &MessageRequest, - apply_stall_timeout: bool, - ) -> Result, RuntimeError> { - let mut stream = self - .client - .stream_message(message_request) - .await - .map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })?; - let mut stdout = io::stdout(); - let mut sink = io::sink(); - let out: &mut dyn Write = if self.emit_output { - &mut stdout - } else { - &mut sink - }; - let renderer = TerminalRenderer::new(); - let mut markdown_stream = MarkdownStreamState::default(); - let mut events = Vec::new(); - let mut pending_tool: Option<(String, String, String)> = None; - let mut block_has_thinking_summary = false; - let mut saw_stop = false; - let mut received_any_event = false; - - loop { - let next = if apply_stall_timeout && !received_any_event { - match tokio::time::timeout(POST_TOOL_STALL_TIMEOUT, stream.next_event()).await { - Ok(inner) => inner.map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })?, - Err(_elapsed) => { - return Err(RuntimeError::new( - "post-tool stall: model did not respond within timeout", - )); - } - } - } else { - stream.next_event().await.map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })? - }; - - let Some(event) = next else { - break; - }; - received_any_event = true; - - match event { - ApiStreamEvent::MessageStart(start) => { - for block in start.message.content { - push_output_block( - block, - out, - &mut events, - &mut pending_tool, - true, - &mut block_has_thinking_summary, - )?; - } - } - ApiStreamEvent::ContentBlockStart(start) => { - push_output_block( - start.content_block, - out, - &mut events, - &mut pending_tool, - true, - &mut block_has_thinking_summary, - )?; - } - ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta { - ContentBlockDelta::TextDelta { text } => { - if !text.is_empty() { - if let Some(progress_reporter) = &self.progress_reporter { - progress_reporter.mark_text_phase(&text); - } - if let Some(rendered) = markdown_stream.push(&renderer, &text) { - write!(out, "{rendered}") - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - } - events.push(AssistantEvent::TextDelta(text)); - } - } - ContentBlockDelta::InputJsonDelta { partial_json } => { - if let Some((_, _, input)) = &mut pending_tool { - input.push_str(&partial_json); - } - } - ContentBlockDelta::ThinkingDelta { thinking } => { - if !block_has_thinking_summary { - render_thinking_block_summary(out, None, false)?; - block_has_thinking_summary = true; - } - events.push(AssistantEvent::ThinkingDelta(thinking)); - } - ContentBlockDelta::SignatureDelta { signature } => { - events.push(AssistantEvent::SignatureDelta(signature)); - } - }, - ApiStreamEvent::ContentBlockStop(_) => { - block_has_thinking_summary = false; - if let Some(rendered) = markdown_stream.flush(&renderer) { - write!(out, "{rendered}") - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - } - if let Some((id, name, input)) = pending_tool.take() { - if let Some(progress_reporter) = &self.progress_reporter { - progress_reporter.mark_tool_phase(&name, &input); - } - // Display tool call now that input is fully accumulated - writeln!(out, "\n{}", format_tool_call_start(&name, &input)) - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - events.push(AssistantEvent::ToolUse { id, name, input }); - } - } - ApiStreamEvent::MessageDelta(delta) => { - events.push(AssistantEvent::Usage(delta.usage.token_usage())); - } - ApiStreamEvent::MessageStop(_) => { - saw_stop = true; - if let Some(rendered) = markdown_stream.flush(&renderer) { - write!(out, "{rendered}") - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - } - events.push(AssistantEvent::MessageStop); - } - } - } - - push_prompt_cache_record(&self.client, &mut events); - - if !saw_stop - && events.iter().any(|event| { - matches!(event, AssistantEvent::TextDelta(text) if !text.is_empty()) - || matches!(event, AssistantEvent::ToolUse { .. }) - }) - { - events.push(AssistantEvent::MessageStop); - } - - if events - .iter() - .any(|event| matches!(event, AssistantEvent::MessageStop)) - { - return Ok(events); - } - - let response = self - .client - .send_message(&MessageRequest { - stream: false, - ..message_request.clone() - }) - .await - .map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })?; - let mut events = response_to_events(response, out)?; - push_prompt_cache_record(&self.client, &mut events); - Ok(events) - } -} diff --git a/anthropic_impl.rs b/anthropic_impl.rs deleted file mode 100644 index ed5d01fb..00000000 --- a/anthropic_impl.rs +++ /dev/null @@ -1,243 +0,0 @@ -impl AnthropicRuntimeClient { - fn new( - session_id: &str, - model: String, - enable_tools: bool, - emit_output: bool, - allowed_tools: Option, - tool_registry: GlobalToolRegistry, - progress_reporter: Option, - ) -> Result> { - // Dispatch to the correct provider at construction time. - // `ApiProviderClient` (exposed by the api crate as - // `ProviderClient`) is an enum over Anthropic / xAI / OpenAI - // variants, where xAI and OpenAI both use the OpenAI-compat - // wire format under the hood. We consult - // `detect_provider_kind(&resolved_model)` so model-name prefix - // routing (`openai/`, `gpt-`, `grok`, `qwen/`) wins over - // env-var presence. - // - // For Anthropic we build the client directly instead of going - // through `ApiProviderClient::from_model_with_anthropic_auth` - // so we can explicitly apply `api::read_base_url()` — that - // reads `ANTHROPIC_BASE_URL` and is required for the local - // mock-server test harness - // (`crates/rusty-claude-cli/tests/compact_output.rs`) to point - // claw at its fake Anthropic endpoint. We also attach a - // session-scoped prompt cache on the Anthropic path; the - // prompt cache is Anthropic-only so non-Anthropic variants - // skip it. - let resolved_model = api::resolve_model_alias(&model); - let client = match detect_provider_kind(&resolved_model) { - ProviderKind::Anthropic => { - let auth = resolve_cli_auth_source()?; - let inner = AnthropicClient::from_auth(auth) - .with_base_url(api::read_base_url()) - .with_prompt_cache(PromptCache::new(session_id)); - ApiProviderClient::Anthropic(inner) - } - ProviderKind::Xai | ProviderKind::OpenAi => { - // The api crate's `ProviderClient::from_model_with_anthropic_auth` - // with `None` for the anthropic auth routes via - // `detect_provider_kind` and builds an - // `OpenAiCompatClient::from_env` with the matching - // `OpenAiCompatConfig` (openai / xai / dashscope). - // That reads the correct API-key env var and BASE_URL - // override internally, so this one call covers OpenAI, - // OpenRouter, xAI, DashScope, Ollama, and any other - // OpenAI-compat endpoint users configure via - // `OPENAI_BASE_URL` / `XAI_BASE_URL` / `DASHSCOPE_BASE_URL`. - ApiProviderClient::from_model_with_anthropic_auth(&resolved_model, None)? - } - }; - Ok(Self { - runtime: tokio::runtime::Runtime::new()?, - client, - session_id: session_id.to_string(), - model, - enable_tools, - emit_output, - allowed_tools, - tool_registry, - progress_reporter, - reasoning_effort: None, - }) - } - - fn set_reasoning_effort(&mut self, effort: Option) { - self.reasoning_effort = effort; - } -} -impl AnthropicRuntimeClient { - /// Consume a single streaming response, optionally applying a stall - /// timeout on the first event for post-tool continuations. - #[allow(clippy::too_many_lines)] - async fn consume_stream( - &self, - message_request: &MessageRequest, - apply_stall_timeout: bool, - ) -> Result, RuntimeError> { - let mut stream = self - .client - .stream_message(message_request) - .await - .map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })?; - let mut stdout = io::stdout(); - let mut sink = io::sink(); - let out: &mut dyn Write = if self.emit_output { - &mut stdout - } else { - &mut sink - }; - let renderer = TerminalRenderer::new(); - let mut markdown_stream = MarkdownStreamState::default(); - let mut events = Vec::new(); - let mut pending_tool: Option<(String, String, String)> = None; - let mut block_has_thinking_summary = false; - let mut saw_stop = false; - let mut received_any_event = false; - - loop { - let next = if apply_stall_timeout && !received_any_event { - match tokio::time::timeout(POST_TOOL_STALL_TIMEOUT, stream.next_event()).await { - Ok(inner) => inner.map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })?, - Err(_elapsed) => { - return Err(RuntimeError::new( - "post-tool stall: model did not respond within timeout", - )); - } - } - } else { - stream.next_event().await.map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })? - }; - - let Some(event) = next else { - break; - }; - received_any_event = true; - - match event { - ApiStreamEvent::MessageStart(start) => { - for block in start.message.content { - push_output_block( - block, - out, - &mut events, - &mut pending_tool, - true, - &mut block_has_thinking_summary, - )?; - } - } - ApiStreamEvent::ContentBlockStart(start) => { - push_output_block( - start.content_block, - out, - &mut events, - &mut pending_tool, - true, - &mut block_has_thinking_summary, - )?; - } - ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta { - ContentBlockDelta::TextDelta { text } => { - if !text.is_empty() { - if let Some(progress_reporter) = &self.progress_reporter { - progress_reporter.mark_text_phase(&text); - } - if let Some(rendered) = markdown_stream.push(&renderer, &text) { - write!(out, "{rendered}") - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - } - events.push(AssistantEvent::TextDelta(text)); - } - } - ContentBlockDelta::InputJsonDelta { partial_json } => { - if let Some((_, _, input)) = &mut pending_tool { - input.push_str(&partial_json); - } - } - ContentBlockDelta::ThinkingDelta { thinking } => { - if !block_has_thinking_summary { - render_thinking_block_summary(out, None, false)?; - block_has_thinking_summary = true; - } - events.push(AssistantEvent::ThinkingDelta(thinking)); - } - ContentBlockDelta::SignatureDelta { signature } => { - events.push(AssistantEvent::SignatureDelta(signature)); - } - }, - ApiStreamEvent::ContentBlockStop(_) => { - block_has_thinking_summary = false; - if let Some(rendered) = markdown_stream.flush(&renderer) { - write!(out, "{rendered}") - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - } - if let Some((id, name, input)) = pending_tool.take() { - if let Some(progress_reporter) = &self.progress_reporter { - progress_reporter.mark_tool_phase(&name, &input); - } - // Display tool call now that input is fully accumulated - writeln!(out, "\n{}", format_tool_call_start(&name, &input)) - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - events.push(AssistantEvent::ToolUse { id, name, input }); - } - } - ApiStreamEvent::MessageDelta(delta) => { - events.push(AssistantEvent::Usage(delta.usage.token_usage())); - } - ApiStreamEvent::MessageStop(_) => { - saw_stop = true; - if let Some(rendered) = markdown_stream.flush(&renderer) { - write!(out, "{rendered}") - .and_then(|()| out.flush()) - .map_err(|error| RuntimeError::new(error.to_string()))?; - } - events.push(AssistantEvent::MessageStop); - } - } - } - - push_prompt_cache_record(&self.client, &mut events); - - if !saw_stop - && events.iter().any(|event| { - matches!(event, AssistantEvent::TextDelta(text) if !text.is_empty()) - || matches!(event, AssistantEvent::ToolUse { .. }) - }) - { - events.push(AssistantEvent::MessageStop); - } - - if events - .iter() - .any(|event| matches!(event, AssistantEvent::MessageStop)) - { - return Ok(events); - } - - let response = self - .client - .send_message(&MessageRequest { - stream: false, - ..message_request.clone() - }) - .await - .map_err(|error| { - RuntimeError::new(format_user_visible_api_error(&self.session_id, &error)) - })?; - let mut events = response_to_events(response, out)?; - push_prompt_cache_record(&self.client, &mut events); - Ok(events) - } -} diff --git a/anthropic_struct.rs b/anthropic_struct.rs deleted file mode 100644 index 6da0a6d8..00000000 --- a/anthropic_struct.rs +++ /dev/null @@ -1,12 +0,0 @@ -struct AnthropicRuntimeClient { - runtime: tokio::runtime::Runtime, - client: ApiProviderClient, - session_id: String, - model: String, - enable_tools: bool, - emit_output: bool, - allowed_tools: Option, - tool_registry: GlobalToolRegistry, - progress_reporter: Option, - reasoning_effort: Option, -} diff --git a/anthropic_trait_impl.rs b/anthropic_trait_impl.rs deleted file mode 100644 index a4476c5c..00000000 --- a/anthropic_trait_impl.rs +++ /dev/null @@ -1,49 +0,0 @@ -impl ApiClient for AnthropicRuntimeClient { - #[allow(clippy::too_many_lines)] - fn stream(&mut self, request: ApiRequest) -> Result, RuntimeError> { - if let Some(progress_reporter) = &self.progress_reporter { - progress_reporter.mark_model_phase(); - } - let is_post_tool = request_ends_with_tool_result(&request); - let message_request = MessageRequest { - model: self.model.clone(), - max_tokens: max_tokens_for_model(&self.model), - messages: convert_messages(&request.messages), - system: (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n")), - tools: self - .enable_tools - .then(|| filter_tool_specs(&self.tool_registry, self.allowed_tools.as_ref())), - tool_choice: self.enable_tools.then_some(ToolChoice::Auto), - stream: true, - reasoning_effort: self.reasoning_effort.clone(), - ..Default::default() - }; - - self.runtime.block_on(async { - // When resuming after tool execution, apply a stall timeout on the - // first stream event. If the model does not respond within the - // deadline we drop the stalled connection and re-send the request as - // a continuation nudge (one retry only). - let max_attempts: usize = if is_post_tool { 2 } else { 1 }; - - for attempt in 1..=max_attempts { - let result = self - .consume_stream(&message_request, is_post_tool && attempt == 1) - .await; - match result { - Ok(events) => return Ok(events), - Err(error) - if error.to_string().contains("post-tool stall") - && attempt < max_attempts => - { - // Stalled after tool completion — nudge the model by - // re-sending the same request. - } - Err(error) => return Err(error), - } - } - - Err(RuntimeError::new("post-tool continuation nudge exhausted")) - }) - } -} diff --git a/debug.sh b/debug.sh deleted file mode 100644 index 2fd83084..00000000 --- a/debug.sh +++ /dev/null @@ -1,19 +0,0 @@ -cargo check -cargo test -cargo fmt --all - - - -### 跨平台编译 -rustup target add x86_64-unknown-linux-musl -cargo build --target x86_64-unknown-linux-musl --release -cp target/x86_64-unknown-linux-musl/release/claw /usr/local/bin/claw - - - - - - -echo $DASHSCOPE_API_KEY -echo $ANTHROPIC_MODEL -claw "帮我看看这个项目的内容" \ No newline at end of file diff --git a/describe_tool_progress_summarizes_known_tools.rs_chunk b/describe_tool_progress_summarizes_known_tools.rs_chunk deleted file mode 100644 index 270db5bf..00000000 --- a/describe_tool_progress_summarizes_known_tools.rs_chunk +++ /dev/null @@ -1,15 +0,0 @@ -#[test] - fn describe_tool_progress_summarizes_known_tools() { - assert_eq!( - describe_tool_progress("read_file", r#"{"path":"src/main.rs"}"#), - "reading src/main.rs" - ); - assert!( - describe_tool_progress("bash", r#"{"command":"cargo test -p rusty-claude-cli"}"#) - .contains("cargo test -p rusty-claude-cli") - ); - assert_eq!( - describe_tool_progress("grep_search", r#"{"pattern":"ultraplan","path":"rust"}"#), - "grep `ultraplan` in rust" - ); - } \ No newline at end of file diff --git a/extract_client.py b/extract_client.py deleted file mode 100644 index 9fd04321..00000000 --- a/extract_client.py +++ /dev/null @@ -1,67 +0,0 @@ -import re - -MAIN_FILE = "rust/crates/rusty-claude-cli/src/main.rs" - -with open(MAIN_FILE, 'r') as f: - content = f.read() - -def extract_and_remove(pattern, content): - match = re.search(pattern, content, re.MULTILINE | re.DOTALL) - if match: - extracted = match.group(0) - content = content.replace(extracted, "") - return extracted, content - return None, content - -anthropic_client_pattern = r'struct AnthropicRuntimeClient \{.*?\n\}\n\nimpl AnthropicRuntimeClient \{.*?\n\}\n\nimpl runtime::Client for AnthropicRuntimeClient \{.*?\n\}' -anthropic_client_code, content = extract_and_remove(anthropic_client_pattern, content) - -build_runtime_pattern = r'fn build_runtime\(\n session: Session,\n session_id: &str,\n model: String,\n system_prompts: Vec,\n enable_tools: bool,\n emit_output: bool,\n allowed_tools: Option,\n permission_mode: PermissionMode,\n reasoning_effort: Option,\n\) -> Result> \{.*?\n\}' -build_runtime_code, content = extract_and_remove(build_runtime_pattern, content) - -build_runtime_with_plugin_state_pattern = r'fn build_runtime_with_plugin_state\(\n session: Session,\n session_id: &str,\n model: String,\n system_prompts: Vec,\n enable_tools: bool,\n emit_output: bool,\n allowed_tools: Option,\n permission_mode: PermissionMode,\n reasoning_effort: Option,\n plugin_state: RuntimePluginState,\n\) -> Result> \{.*?\n\}' -build_runtime_with_plugin_state_code, content = extract_and_remove(build_runtime_with_plugin_state_pattern, content) - -build_runtime_plugin_state_pattern = r'fn build_runtime_plugin_state\(\) -> Result> \{.*?\n\}' -build_runtime_plugin_state_code, content = extract_and_remove(build_runtime_plugin_state_pattern, content) - -build_runtime_plugin_state_with_loader_pattern = r'fn build_runtime_plugin_state_with_loader\(\n cwd: &Path,\n loader: &ConfigLoader,\n runtime_config: &runtime::RuntimeConfig,\n\) -> Result> \{.*?\n\}' -build_runtime_plugin_state_with_loader_code, content = extract_and_remove(build_runtime_plugin_state_with_loader_pattern, content) - -runtime_plugin_state_pattern = r'struct RuntimePluginState \{.*?\n\}' -runtime_plugin_state_code, content = extract_and_remove(runtime_plugin_state_pattern, content) - -build_runtime_mcp_state_pattern = r'fn build_runtime_mcp_state\(\n mcp_config: Option<&runtime::McpConfig>,\n cwd: &Path,\n\) -> Option>> \{.*?\n\}' -build_runtime_mcp_state_code, content = extract_and_remove(build_runtime_mcp_state_pattern, content) - -mcp_wrapper_tools_pattern = r'fn mcp_wrapper_tool_definitions\(\) -> Vec \{.*?\n\}' -mcp_wrapper_tools_code, content = extract_and_remove(mcp_wrapper_tools_pattern, content) - -mcp_runtime_tool_definition_pattern = r'fn mcp_runtime_tool_definition\(tool: &runtime::ManagedMcpTool\) -> RuntimeToolDefinition \{.*?\n\}' -mcp_runtime_tool_definition_code, content = extract_and_remove(mcp_runtime_tool_definition_pattern, content) - -mcp_permission_mode_pattern = r'fn permission_mode_for_mcp_tool\(tool: &McpTool\) -> PermissionMode \{.*?\n\}' -mcp_permission_mode_code, content = extract_and_remove(mcp_permission_mode_pattern, content) - -mcp_annotation_flag_pattern = r'fn mcp_annotation_flag\(tool: &McpTool, key: &str\) -> bool \{.*?\n\}' -mcp_annotation_flag_code, content = extract_and_remove(mcp_annotation_flag_pattern, content) - -mcp_state_pattern = r'struct RuntimeMcpState \{.*?\n\}\n\nimpl RuntimeMcpState \{.*?\n\}' -mcp_state_code, content = extract_and_remove(mcp_state_pattern, content) - -with open(MAIN_FILE, 'w') as f: - f.write(content) - -with open('client_extracted.rs', 'w') as f: - blocks = [ - anthropic_client_code, build_runtime_code, build_runtime_with_plugin_state_code, - build_runtime_plugin_state_code, build_runtime_plugin_state_with_loader_code, - runtime_plugin_state_code, build_runtime_mcp_state_code, mcp_wrapper_tools_code, - mcp_runtime_tool_definition_code, mcp_permission_mode_code, mcp_annotation_flag_code, - mcp_state_code - ] - for block in blocks: - if block: - f.write(block + "\n\n") - -print("Client extraction complete") diff --git a/extract_executor.py b/extract_executor.py deleted file mode 100644 index 3c486048..00000000 --- a/extract_executor.py +++ /dev/null @@ -1,26 +0,0 @@ -import re - -MAIN_FILE = "rust/crates/rusty-claude-cli/src/main.rs" - -with open(MAIN_FILE, 'r') as f: - content = f.read() - -def extract_and_remove(pattern, content): - match = re.search(pattern, content, re.MULTILINE | re.DOTALL) - if match: - extracted = match.group(0) - content = content.replace(extracted, "") - return extracted, content - return None, content - -executor_pattern = r'struct CliToolExecutor \{.*?\n\}\n\nimpl CliToolExecutor \{.*?\n\}\n\nimpl ToolExecutor for CliToolExecutor \{.*?\n\}' -executor_code, content = extract_and_remove(executor_pattern, content) - -with open(MAIN_FILE, 'w') as f: - f.write(content) - -with open('executor_extracted.rs', 'w') as f: - if executor_code: - f.write(executor_code + "\n\n") - -print("Executor extraction complete") diff --git a/extract_stream.py b/extract_stream.py deleted file mode 100644 index afc4b656..00000000 --- a/extract_stream.py +++ /dev/null @@ -1,42 +0,0 @@ -import re -import os - -MAIN_FILE = "rust/crates/rusty-claude-cli/src/main.rs" - -with open(MAIN_FILE, 'r') as f: - content = f.read() - -def extract_and_remove(pattern, content): - match = re.search(pattern, content, re.MULTILINE | re.DOTALL) - if match: - extracted = match.group(0) - content = content.replace(extracted, "") - return extracted, content - return None, content - -consume_stream_pattern = r' /// Consume a single streaming response, optionally applying a stall\n /// timeout on the first event for post-tool continuations\.\n #\[allow\(clippy::too_many_lines\)\]\n async fn consume_stream\(\n &self,\n message_request: &MessageRequest,\n apply_stall_timeout: bool,\n \) -> Result, RuntimeError> \{.*?\n \}' -consume_stream_code, content = extract_and_remove(consume_stream_pattern, content) - -response_to_events_pattern = r'fn response_to_events\(\n response: MessageResponse,\n out: &mut \(impl Write \+ \?Sized\),\n\) -> Result, RuntimeError> \{.*?\n\}' -response_to_events_code, content = extract_and_remove(response_to_events_pattern, content) - -push_prompt_cache_record_pattern = r'fn push_prompt_cache_record\(client: &ApiProviderClient, events: &mut Vec\) \{.*?\n\}' -push_prompt_cache_record_code, content = extract_and_remove(push_prompt_cache_record_pattern, content) - -prompt_cache_record_to_runtime_event_pattern = r'fn prompt_cache_record_to_runtime_event\(\n record: api::PromptCacheRecord,\n\) -> Option \{.*?\n\}' -prompt_cache_record_to_runtime_event_code, content = extract_and_remove(prompt_cache_record_to_runtime_event_pattern, content) - -request_ends_with_tool_result_pattern = r'/// Returns `true` when the conversation ends with a tool-result message,\n/// meaning the model is expected to continue after tool execution\.\nfn request_ends_with_tool_result\(request: &ApiRequest\) -> bool \{.*?\n\}' -request_ends_with_tool_result_code, content = extract_and_remove(request_ends_with_tool_result_pattern, content) - -with open(MAIN_FILE, 'w') as f: - f.write(content) - -with open('stream_extracted.rs', 'w') as f: - if consume_stream_code: f.write(consume_stream_code + "\n\n") - if response_to_events_code: f.write(response_to_events_code + "\n\n") - if push_prompt_cache_record_code: f.write(push_prompt_cache_record_code + "\n\n") - if prompt_cache_record_to_runtime_event_code: f.write(prompt_cache_record_to_runtime_event_code + "\n\n") - if request_ends_with_tool_result_code: f.write(request_ends_with_tool_result_code + "\n\n") - -print("Extraction complete") diff --git a/extract_tests.py b/extract_tests.py deleted file mode 100644 index 273af909..00000000 --- a/extract_tests.py +++ /dev/null @@ -1,30 +0,0 @@ -import re - -with open('rust/crates/rusty-claude-cli/src/main_tests.rs', 'r') as f: - content = f.read() - -def extract_test(name): - pattern = r'(#\[test\]\s+fn ' + name + r'\b.*?^ \})' - match = re.search(pattern, content, re.MULTILINE | re.DOTALL) - if match: - return match.group(1) - return None - -tests = [ - "resolves_known_model_aliases", - "user_defined_aliases_resolve_before_provider_dispatch", - "resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit", - "resolve_repl_model_falls_back_to_anthropic_model_env_when_default", - "resolve_repl_model_returns_default_when_env_unset_and_no_config", - "permission_policy_uses_plugin_tool_permissions", - "describe_tool_progress_summarizes_known_tools" -] - -for test in tests: - extracted = extract_test(test) - if extracted: - with open(f"{test}.rs_chunk", "w") as out: - out.write(extracted) - else: - print(f"Test not found: {test}") - diff --git a/fix_all.py b/fix_all.py deleted file mode 100644 index e7095029..00000000 --- a/fix_all.py +++ /dev/null @@ -1,40 +0,0 @@ -import re - -STREAM_FILE = "rust/crates/rusty-claude-cli/src/execution/stream.rs" -CLIENT_FILE = "rust/crates/rusty-claude-cli/src/execution/client.rs" -EXECUTOR_FILE = "rust/crates/rusty-claude-cli/src/execution/executor.rs" - -with open(STREAM_FILE, 'r') as f: - stream_content = f.read() - -# Extract consume_stream -consume_stream_pattern = r' /// Consume a single streaming response.*?\}\n\n' -match = re.search(consume_stream_pattern, stream_content, re.DOTALL | re.MULTILINE) -if match: - consume_stream_code = match.group(0) - stream_content = stream_content.replace(consume_stream_code, "") - - with open(STREAM_FILE, 'w') as f: - f.write(stream_content) - - with open(CLIENT_FILE, 'r') as f: - client_content = f.read() - - # insert inside impl AnthropicRuntimeClient - client_content = client_content.replace("impl AnthropicRuntimeClient {", "impl AnthropicRuntimeClient {\n" + consume_stream_code) - - # fix trait visibility issues - client_content = client_content.replace("pub(crate) fn deref(", "fn deref(") - client_content = client_content.replace("pub(crate) fn deref_mut(", "fn deref_mut(") - client_content = client_content.replace("pub(crate) fn drop(", "fn drop(") - - with open(CLIENT_FILE, 'w') as f: - f.write(client_content) - -with open(EXECUTOR_FILE, 'r') as f: - executor_content = f.read() -executor_content = executor_content.replace("pub(crate) fn execute(", "fn execute(") -with open(EXECUTOR_FILE, 'w') as f: - f.write(executor_content) - -print("Fixed.") diff --git a/fix_anthropic.py b/fix_anthropic.py deleted file mode 100644 index d6dd508a..00000000 --- a/fix_anthropic.py +++ /dev/null @@ -1,20 +0,0 @@ -import re - -with open('anthropic_full.rs', 'r') as f: - content = f.read() - -content = re.sub(r'^struct ', r'pub(crate) struct ', content, flags=re.MULTILINE) -content = re.sub(r'^(\s+)(async )?fn ', r'\1pub(crate) \2fn ', content, flags=re.MULTILINE) - -# except for trait methods -content = re.sub(r'pub\(crate\) fn name\(', r'fn name(', content) -content = re.sub(r'pub\(crate\) async fn process_stream\(', r'async fn process_stream(', content) -content = re.sub(r'pub\(crate\) fn session_id\(', r'fn session_id(', content) -content = re.sub(r'pub\(crate\) fn model\(', r'fn model(', content) -content = re.sub(r'pub\(crate\) fn enable_tools\(', r'fn enable_tools(', content) - -with open('anthropic_full.rs', 'w') as f: - f.write(content) - -with open('rust/crates/rusty-claude-cli/src/execution/client.rs', 'a') as f: - f.write("\n" + content + "\n") diff --git a/fix_diagnostics.sh b/fix_diagnostics.sh deleted file mode 100644 index d3a6230c..00000000 --- a/fix_diagnostics.sh +++ /dev/null @@ -1,9 +0,0 @@ -sed -i '' '1i\ -use crate::{CliOutputFormat, OFFICIAL_REPO_SLUG, DEPRECATED_INSTALL_COMMAND, OFFICIAL_REPO_URL, resolve_cli_auth_source_for_cwd};\ -use tools::{mvp_tool_specs, execute_tool};\ -use runtime::{McpTool, McpServerSpec, McpServer, load_oauth_credentials};\ -' rust/crates/rusty-claude-cli/src/diagnostics/mod.rs - -sed -i '' 's/fn run_doctor(/pub(crate) fn run_doctor(/g' rust/crates/rusty-claude-cli/src/diagnostics/mod.rs -sed -i '' 's/fn run_worker_state(/pub(crate) fn run_worker_state(/g' rust/crates/rusty-claude-cli/src/diagnostics/mod.rs -sed -i '' 's/fn run_mcp_serve(/pub(crate) fn run_mcp_serve(/g' rust/crates/rusty-claude-cli/src/diagnostics/mod.rs diff --git a/fix_models_test.py b/fix_models_test.py deleted file mode 100644 index 83aa46ca..00000000 --- a/fix_models_test.py +++ /dev/null @@ -1,52 +0,0 @@ -import re - -MODELS_FILE = "rust/crates/rusty-claude-cli/src/config/models.rs" - -with open(MODELS_FILE, 'r') as f: - content = f.read() - -content = content.replace("use runtime::test_utils::{env_lock, temp_dir, with_current_dir};", """ - use std::sync::{Mutex, MutexGuard, OnceLock}; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::path::{Path, PathBuf}; - use std::fs; - - fn env_lock() -> MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - } - - fn cwd_lock() -> MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - } - - fn temp_dir() -> PathBuf { - static COUNTER: AtomicUsize = AtomicUsize::new(0); - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .subsec_nanos(); - let unique = COUNTER.fetch_add(1, Ordering::Relaxed); - std::env::temp_dir().join(format!("rusty-claude-cli-{nanos}-{unique}")) - } - - fn with_current_dir(cwd: &Path, f: impl FnOnce() -> T) -> T { - let _guard = cwd_lock(); - let previous = std::env::current_dir().expect("cwd should load"); - std::env::set_current_dir(cwd).expect("cwd should change"); - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); - std::env::set_current_dir(previous).expect("cwd should restore"); - match result { - Ok(value) => value, - Err(payload) => std::panic::resume_unwind(payload), - } - } -""") - -with open(MODELS_FILE, 'w') as f: - f.write(content) diff --git a/fix_progress.sh b/fix_progress.sh deleted file mode 100644 index c29881ea..00000000 --- a/fix_progress.sh +++ /dev/null @@ -1,8 +0,0 @@ -sed -i '' 's/^struct InternalPromptProgressState/pub(crate) struct InternalPromptProgressState/g' rust/crates/rusty-claude-cli/src/ui/progress.rs -sed -i '' 's/^enum InternalPromptProgressEvent/pub(crate) enum InternalPromptProgressEvent/g' rust/crates/rusty-claude-cli/src/ui/progress.rs -sed -i '' 's/^struct InternalPromptProgressShared/pub(crate) struct InternalPromptProgressShared/g' rust/crates/rusty-claude-cli/src/ui/progress.rs -sed -i '' 's/^struct InternalPromptProgressReporter/pub(crate) struct InternalPromptProgressReporter/g' rust/crates/rusty-claude-cli/src/ui/progress.rs -sed -i '' 's/^struct InternalPromptProgressRun/pub(crate) struct InternalPromptProgressRun/g' rust/crates/rusty-claude-cli/src/ui/progress.rs -sed -i '' 's/^struct CliHookProgressReporter/pub(crate) struct CliHookProgressReporter/g' rust/crates/rusty-claude-cli/src/ui/progress.rs -sed -ised -i '' 's/^struct InternalPromptProgrplsed -i '' 's/^enum InternalPromptProgressEvent/pub(crate) enum InternalPromptProgressEvent/g' rust/crates/rusty-claude-cli/src/ui/progress.rs -seresed -i '' 's/^struct InternalPromptProgressShared/pub(crate) struct InternalPromptProgressShared/g' rust/crates/rusty-claude-cli/src/ui/prog sed -i "''" "'s/^struct InternalPromptProgressReporter/pub(crate) struct InternalPromptProgressReporter/g'" rust/crates/rusty-claude-cli/src/ui/progrerased -i "''" "'s/^struct InternalPromptProgressRun/pub(crate) struct InternalPromptProgressRun/g'" rust/crates/rusty-claude-cli/src/ui/progress.rs \ No newline at end of file diff --git a/fix_prompter.py b/fix_prompter.py deleted file mode 100644 index 2ad26cf2..00000000 --- a/fix_prompter.py +++ /dev/null @@ -1,20 +0,0 @@ -import re - -with open("rust/crates/rusty-claude-cli/src/permissions/prompter.rs", "r") as f: - content = f.read() - -content = re.sub(r"^fn (parse_permission_mode_arg|permission_mode_from_label|permission_mode_from_resolved|default_permission_mode|config_permission_mode_for_current_dir|permission_policy)\(", r"pub(crate) fn \1(", content, flags=re.M) -content = re.sub(r"^struct CliPermissionPrompter", r"pub(crate) struct CliPermissionPrompter", content, flags=re.M) -content = re.sub(r"fn new\(", r"pub(crate) fn new(", content) - -prefix = """use std::env; -use std::io::{self, Write}; -use runtime::{PermissionMode, ResolvedPermissionMode, ConfigLoader, PermissionPolicy}; -use tools::GlobalToolRegistry; - -""" - -with open("rust/crates/rusty-claude-cli/src/permissions/prompter.rs", "w") as f: - f.write(prefix + content) - -print("done") diff --git a/fix_stream_client.py b/fix_stream_client.py deleted file mode 100644 index f29c0faa..00000000 --- a/fix_stream_client.py +++ /dev/null @@ -1,20 +0,0 @@ -import os - -with open('rust/crates/rusty-claude-cli/src/execution/stream.rs', 'r') as f: - stream_content = f.read() - -import re - -# extract consume_stream from stream.rs -match = re.search(r'impl AnthropicRuntimeClient \{.*?\n\}', stream_content, re.MULTILINE | re.DOTALL) -if match: - consume_stream_code = match.group(0) - stream_content = stream_content.replace(consume_stream_code, "") - - with open('rust/crates/rusty-claude-cli/src/execution/stream.rs', 'w') as f: - f.write(stream_content) - - with open('rust/crates/rusty-claude-cli/src/execution/client.rs', 'a') as f: - f.write("\n" + consume_stream_code + "\n") - -print("Fixed stream and client") diff --git a/make_pub_crate.py b/make_pub_crate.py deleted file mode 100644 index 625e10b8..00000000 --- a/make_pub_crate.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import re - -files = [ - 'rust/crates/rusty-claude-cli/src/execution/client.rs', - 'rust/crates/rusty-claude-cli/src/execution/stream.rs', - 'rust/crates/rusty-claude-cli/src/execution/executor.rs' -] - -for file in files: - with open(file, 'r') as f: - content = f.read() - - # Change fn to pub(crate) fn - content = re.sub(r'^(async )?fn ', r'pub(crate) \1fn ', content, flags=re.MULTILINE) - content = re.sub(r'pub\(crate\) pub\(crate\) ', r'pub(crate) ', content) - - # Inside impl blocks, we also want pub(crate) fn - content = re.sub(r'^(\s+)(async )?fn ', r'\1pub(crate) \2fn ', content, flags=re.MULTILINE) - - # Structs - content = re.sub(r'^struct ', r'pub(crate) struct ', content, flags=re.MULTILINE) - - # Struct fields - # Match structs block and add pub(crate) to fields - def repl_struct(m): - inner = m.group(2) - # replace field definitions: "name: type," - inner = re.sub(r'^(\s+)([a-zA-Z0-9_]+)\s*:\s*', r'\1pub(crate) \2: ', inner, flags=re.MULTILINE) - return m.group(1) + inner + m.group(3) - - content = re.sub(r'(pub\(crate\) struct [a-zA-Z0-9_]+ \{)(.*?)(\})', repl_struct, content, flags=re.DOTALL) - - with open(file, 'w') as f: - f.write(content) - -print("Visibility updated.") diff --git a/move_tests.py b/move_tests.py deleted file mode 100644 index 7fd3af5f..00000000 --- a/move_tests.py +++ /dev/null @@ -1,114 +0,0 @@ -import re -import os - -MAIN_TESTS_FILE = "rust/crates/rusty-claude-cli/src/main_tests.rs" -CONFIG_MODELS_FILE = "rust/crates/rusty-claude-cli/src/config/models.rs" -PERMISSIONS_PROMPTER_FILE = "rust/crates/rusty-claude-cli/src/permissions/prompter.rs" -UI_PROGRESS_FILE = "rust/crates/rusty-claude-cli/src/ui/progress.rs" - -with open(MAIN_TESTS_FILE, 'r') as f: - content = f.read() - -def remove_test(name, content): - pattern = r'#\[test\]\s+fn ' + name + r'\b.*?^ \}' - new_content, count = re.subn(pattern, '', content, flags=re.MULTILINE | re.DOTALL) - if count == 0: - print(f"Warning: Test {name} not found to remove.") - return new_content - -tests_for_models = [ - "resolves_known_model_aliases", - "user_defined_aliases_resolve_before_provider_dispatch", - "resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit", - "resolve_repl_model_falls_back_to_anthropic_model_env_when_default", - "resolve_repl_model_returns_default_when_env_unset_and_no_config", -] - -for test in tests_for_models: - content = remove_test(test, content) - -content = remove_test("permission_policy_uses_plugin_tool_permissions", content) -content = remove_test("describe_tool_progress_summarizes_known_tools", content) - -# Remove imports from main_tests.rs -imports_to_remove = [ - "resolve_model_alias,", "resolve_model_alias_with_config,", "resolve_repl_model,", - "permission_policy,", "describe_tool_progress," -] -for imp in imports_to_remove: - content = content.replace(imp, "") - -with open(MAIN_TESTS_FILE, 'w') as f: - f.write(content) - -# --- Append to src/config/models.rs --- -models_tests = [] -for test in tests_for_models: - with open(f"{test}.rs_chunk", "r") as f: - models_tests.append(" " + f.read().replace("\n", "\n ").strip()) - -models_test_mod = """ -#[cfg(test)] -mod tests { - use super::*; - use runtime::test_utils::{env_lock, temp_dir, with_current_dir}; - -""" + "\n\n".join(models_tests) + "\n}\n" - -with open(CONFIG_MODELS_FILE, 'a') as f: - f.write(models_test_mod) - -# --- Append to src/permissions/prompter.rs --- -with open("permission_policy_uses_plugin_tool_permissions.rs_chunk", "r") as f: - prompter_test = " " + f.read().replace("\n", "\n ").strip() - -prompter_test_mod = """ -#[cfg(test)] -mod tests { - use super::*; - use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission}; - use serde_json::json; - use tools::GlobalToolRegistry; - use runtime::{PermissionMode, RuntimeFeatureConfig}; - - fn registry_with_plugin_tool() -> GlobalToolRegistry { - GlobalToolRegistry::with_plugin_tools(vec![PluginTool::new( - "plugin-demo@external", - "plugin-demo", - PluginToolDefinition { - name: "plugin_echo".to_string(), - description: Some("Echo plugin payload".to_string()), - input_schema: json!({ - "type": "object", - "properties": { - "message": { "type": "string" } - }, - "required": ["message"], - "additionalProperties": false - }), - }, - "echo".to_string(), - PluginToolPermission::WorkspaceWrite, - )]) - } - -""" + prompter_test + "\n}\n" - -with open(PERMISSIONS_PROMPTER_FILE, 'a') as f: - f.write(prompter_test_mod) - -# --- Append to src/ui/progress.rs --- -with open("describe_tool_progress_summarizes_known_tools.rs_chunk", "r") as f: - progress_test = " " + f.read().replace("\n", "\n ").strip() - -progress_test_mod = """ -#[cfg(test)] -mod tests { - use super::*; - -""" + progress_test + "\n}\n" - -with open(UI_PROGRESS_FILE, 'a') as f: - f.write(progress_test_mod) - -print("Done moving tests.") diff --git a/permission_policy_uses_plugin_tool_permissions.rs_chunk b/permission_policy_uses_plugin_tool_permissions.rs_chunk deleted file mode 100644 index a73cc994..00000000 --- a/permission_policy_uses_plugin_tool_permissions.rs_chunk +++ /dev/null @@ -1,12 +0,0 @@ -#[test] - fn permission_policy_uses_plugin_tool_permissions() { - let feature_config = runtime::RuntimeFeatureConfig::default(); - let policy = permission_policy( - PermissionMode::ReadOnly, - &feature_config, - ®istry_with_plugin_tool(), - ) - .expect("permission policy should build"); - let required = policy.required_mode_for("plugin_echo"); - assert_eq!(required, PermissionMode::WorkspaceWrite); - } \ No newline at end of file diff --git a/prd.json b/prd.json deleted file mode 100644 index 8faaa541..00000000 --- a/prd.json +++ /dev/null @@ -1,356 +0,0 @@ -{ - "version": "1.0", - "description": "Clawable Coding Harness - Clear roadmap stories and commit each", - "stories": [ - { - "id": "US-001", - "title": "Phase 1.6 - startup-no-evidence evidence bundle + classifier", - "description": "When startup times out, emit typed worker.startup_no_evidence event with evidence bundle including last known worker lifecycle state, pane command, prompt-send timestamp, prompt-acceptance state, trust-prompt detection result, and transport/MCP health summary. Classifier should down-rank into specific failure classes.", - "acceptanceCriteria": [ - "worker.startup_no_evidence event emitted on startup timeout with evidence bundle", - "Evidence bundle includes: last lifecycle state, pane command, prompt-send timestamp, prompt-acceptance state, trust-prompt detection, transport/MCP health", - "Classifier attempts to categorize into: trust_required, prompt_misdelivery, prompt_acceptance_timeout, transport_dead, worker_crashed, or unknown", - "Tests verify evidence bundle structure and classifier behavior" - ], - "passes": true, - "priority": "P0" - }, - { - "id": "US-002", - "title": "Phase 2 - Canonical lane event schema (4.x series)", - "description": "Define typed events for lane lifecycle: lane.started, lane.ready, lane.prompt_misdelivery, lane.blocked, lane.red, lane.green, lane.commit.created, lane.pr.opened, lane.merge.ready, lane.finished, lane.failed, branch.stale_against_main. Also implement event ordering, reconciliation, provenance, deduplication, and projection contracts.", - "acceptanceCriteria": [ - "LaneEvent enum with all required variants defined", - "Event ordering with monotonic sequence metadata attached", - "Event provenance labels (live_lane, test, healthcheck, replay, transport)", - "Session identity completeness at creation (title, workspace, purpose)", - "Duplicate terminal-event suppression with fingerprinting", - "Lane ownership/scope binding in events", - "Nudge acknowledgment with dedupe contract", - "clawhip consumes typed lane events instead of pane scraping" - ], - "passes": true, - "priority": "P0" - }, - { - "id": "US-003", - "title": "Phase 3 - Stale-branch detection before broad verification", - "description": "Before broad test runs, compare current branch to main and detect if known fixes are missing. Emit branch.stale_against_main event and suggest/auto-run rebase/merge-forward.", - "acceptanceCriteria": [ - "Branch freshness comparison against main implemented", - "branch.stale_against_main event emitted when behind", - "Auto-rebase/merge-forward policy integration", - "Avoid misclassifying stale-branch failures as new regressions" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-004", - "title": "Phase 3 - Recovery recipes with ledger", - "description": "Encode automatic recoveries for common failures (trust prompt, prompt misdelivery, stale branch, compile red, MCP startup). Expose recovery attempt ledger with recipe id, attempt count, state, timestamps, failure summary.", - "acceptanceCriteria": [ - "Recovery recipes defined for: trust_prompt_unresolved, prompt_delivered_to_shell, stale_branch, compile_red_after_refactor, MCP_handshake_failure, partial_plugin_startup", - "Recovery attempt ledger with: recipe id, attempt count, state, timestamps, failure summary, escalation reason", - "One automatic recovery attempt before escalation", - "Ledger emitted as structured event data" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-005", - "title": "Phase 4 - Typed task packet format", - "description": "Define structured task packet with fields: objective, scope, repo/worktree, branch policy, acceptance tests, commit policy, reporting contract, escalation policy.", - "acceptanceCriteria": [ - "TaskPacket struct with all required fields", - "TaskScope resolution (workspace/module/single-file/custom)", - "Validation and serialization support", - "Integration into tools/src/lib.rs" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-006", - "title": "Phase 4 - Policy engine for autonomous coding", - "description": "Encode automation rules: if green + scoped diff + review passed -> merge to dev; if stale branch -> merge-forward before broad tests; if startup blocked -> recover once, then escalate; if lane completed -> emit closeout and cleanup session.", - "acceptanceCriteria": [ - "Policy rules engine implemented", - "Rules: green + scoped diff + review -> merge", - "Rules: stale branch -> merge-forward before tests", - "Rules: startup blocked -> recover once, then escalate", - "Rules: lane completed -> closeout and cleanup" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-007", - "title": "Phase 5 - Plugin/MCP lifecycle maturity", - "description": "First-class plugin/MCP lifecycle contract: config validation, startup healthcheck, discovery result, degraded-mode behavior, shutdown/cleanup. Close gaps in end-to-end lifecycle.", - "acceptanceCriteria": [ - "Plugin/MCP config validation contract", - "Startup healthcheck with structured results", - "Discovery result reporting", - "Degraded-mode behavior documented and implemented", - "Shutdown/cleanup contract", - "Partial startup and per-server failures reported structurally" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-008", - "title": "Fix kimi-k2.5 model API compatibility", - "description": "The kimi-k2.5 model (and other kimi models) reject API requests containing the is_error field in tool result messages. The OpenAI-compatible provider currently always includes is_error for all models. Need to make this field conditional based on model support.", - "acceptanceCriteria": [ - "translate_message function accepts model parameter", - "is_error field excluded for kimi models (kimi-k2.5, kimi-k1.5, etc.)", - "is_error field included for models that support it (openai, grok, xai, etc.)", - "build_chat_completion_request passes model to translate_message", - "Tests verify is_error presence/absence based on model", - "cargo test passes", - "cargo clippy passes", - "cargo fmt passes" - ], - "passes": true, - "priority": "P0" - }, - { - "id": "US-009", - "title": "Add unit tests for kimi model compatibility fix", - "description": "During dogfooding we discovered the existing test coverage for model-specific is_error handling is insufficient. Need to add dedicated tests for model_rejects_is_error_field function and translate_message behavior with different models.", - "acceptanceCriteria": [ - "Test model_rejects_is_error_field identifies kimi-k2.5, kimi-k1.5, dashscope/kimi-k2.5", - "Test translate_message includes is_error for gpt-4, grok-3, claude models", - "Test translate_message excludes is_error for kimi models", - "Test build_chat_completion_request produces correct payload for kimi vs non-kimi", - "All new tests pass", - "cargo test --package api passes" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-010", - "title": "Add model compatibility documentation", - "description": "Document which models require special handling (is_error exclusion, reasoning model tuning param stripping, etc.) in a MODEL_COMPATIBILITY.md file for operators and contributors.", - "acceptanceCriteria": [ - "MODEL_COMPATIBILITY.md created in docs/ or repo root", - "Document kimi models is_error exclusion", - "Document reasoning models (o1, o3, grok-3-mini) tuning param stripping", - "Document gpt-5 max_completion_tokens requirement", - "Document qwen model routing through dashscope", - "Cross-reference with existing code comments" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-011", - "title": "Performance optimization: reduce API request serialization overhead", - "description": "The translate_message function creates intermediate JSON Value objects that could be optimized. Profile and optimize the hot path for API request building, especially for conversations with many tool results.", - "acceptanceCriteria": [ - "Profile current request building with criterion or similar", - "Identify bottlenecks in translate_message and build_chat_completion_request", - "Implement optimizations (Vec pre-allocation, reduced cloning, etc.)", - "Benchmark before/after showing improvement", - "No functional changes or API breakage" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-012", - "title": "Trust prompt resolver with allowlist auto-trust", - "description": "Add allowlisted auto-trust behavior for known repos/worktrees. Trust prompts currently block TUI startup and require manual intervention. Implement automatic trust resolution for pre-approved repositories.", - "acceptanceCriteria": [ - "TrustAllowlist config structure with repo patterns", - "Auto-trust behavior for allowlisted repos/worktrees", - "trust_required event emitted when trust prompt detected", - "trust_resolved event emitted when trust is granted", - "Non-allowlisted repos remain gated (manual trust required)", - "Integration with worker boot lifecycle", - "Tests for allowlist matching and event emission" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-013", - "title": "Phase 2 - Session event ordering + terminal-state reconciliation", - "description": "When the same session emits contradictory lifecycle events (idle, error, completed, transport/server-down) in close succession, expose deterministic final truth. Attach monotonic sequence/causal ordering metadata, classify terminal vs advisory events, reconcile duplicate/out-of-order terminal events into one canonical lane outcome.", - "acceptanceCriteria": [ - "Monotonic sequence / causal ordering metadata attached to session lifecycle events", - "Terminal vs advisory event classification implemented", - "Reconcile duplicate or out-of-order terminal events into one canonical outcome", - "Distinguish 'session terminal state unknown because transport died' from real 'completed'", - "Tests verify reconciliation behavior with out-of-order event bursts" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-014", - "title": "Phase 2 - Event provenance / environment labeling", - "description": "Every emitted event should declare its source (live_lane, test, healthcheck, replay, transport) so claws do not mistake test noise for production truth. Include environment/channel label, emitter identity, and confidence/trust level.", - "acceptanceCriteria": [ - "EventProvenance enum with live_lane, test, healthcheck, replay, transport variants", - "Environment/channel label attached to all events", - "Emitter identity field on events", - "Confidence/trust level field for downstream automation", - "Tests verify provenance labeling and filtering" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-015", - "title": "Phase 2 - Session identity completeness at creation time", - "description": "A newly created session should emit stable title, workspace/worktree path, and lane/session purpose at creation time. If any field is not yet known, emit explicit typed placeholder reason rather than bare unknown string.", - "acceptanceCriteria": [ - "Session creation emits stable title, workspace/worktree path, purpose immediately", - "Explicit typed placeholder when fields unknown (not bare 'unknown' strings)", - "Later-enriched metadata reconciles onto same session identity without ambiguity", - "Tests verify session identity completeness and placeholder handling" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-016", - "title": "Phase 2 - Duplicate terminal-event suppression", - "description": "When the same session emits repeated completed/failed/terminal notifications, collapse duplicates before they trigger repeated downstream reactions. Attach canonical terminal-event fingerprint per lane/session outcome.", - "acceptanceCriteria": [ - "Canonical terminal-event fingerprint attached per lane/session outcome", - "Suppress/coalesce repeated terminal notifications within reconciliation window", - "Preserve raw event history for audit while exposing one actionable outcome downstream", - "Surface when later duplicate materially differs from original terminal payload", - "Tests verify deduplication and material difference detection" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-017", - "title": "Phase 2 - Lane ownership / scope binding", - "description": "Each session and lane event should declare who owns it and what workflow scope it belongs to. Attach owner/assignee identity, workflow scope (claw-code-dogfood, external-git-maintenance, infra-health, manual-operator), and mark whether watcher is expected to act, observe only, or ignore.", - "acceptanceCriteria": [ - "Owner/assignee identity attached to sessions and lane events", - "Workflow scope field (claw-code-dogfood, external-git-maintenance, etc.)", - "Watcher action expectation field (act, observe-only, ignore)", - "Preserve scope through session restarts, resumes, and late terminal events", - "Tests verify ownership and scope binding" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-018", - "title": "Phase 2 - Nudge acknowledgment / dedupe contract", - "description": "Periodic clawhip nudges should carry nudge id/cycle id and delivery timestamp. Expose whether claw has already acknowledged or responded for that cycle. Distinguish new nudge, retry nudge, and stale duplicate.", - "acceptanceCriteria": [ - "Nudge id / cycle id and delivery timestamp attached", - "Acknowledgment state exposed (already acknowledged or not)", - "Distinguish new nudge vs retry nudge vs stale duplicate", - "Allow downstream summaries to bind reported pinpoint back to triggering nudge id", - "Tests verify nudge deduplication and acknowledgment tracking" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-019", - "title": "Phase 2 - Stable roadmap-id assignment for newly filed pinpoints", - "description": "When a claw records a new pinpoint/follow-up, assign or expose a stable tracking id immediately. Expose that id in structured event/report payload and preserve across edits, reorderings, and summary compression.", - "acceptanceCriteria": [ - "Canonical roadmap id assigned at filing time", - "Roadmap id exposed in structured event/report payload", - "Same id preserved across edits, reorderings, summary compression", - "Distinguish 'new roadmap filing' from 'update to existing roadmap item'", - "Tests verify stable id assignment and update detection" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-020", - "title": "Phase 2 - Roadmap item lifecycle state contract", - "description": "Each roadmap pinpoint should carry machine-readable lifecycle state (filed, acknowledged, in_progress, blocked, done, superseded). Attach last state-change timestamp and preserve lineage when one pinpoint supersedes or merges into another.", - "acceptanceCriteria": [ - "Lifecycle state enum with filed, acknowledged, in_progress, blocked, done, superseded", - "Last state-change timestamp attached", - "New report can declare first filing, status update, or closure", - "Preserve lineage when one pinpoint supersedes or merges into another", - "Tests verify lifecycle state transitions" - ], - "passes": true, - "priority": "P2" - }, - { - "id": "US-021", - "title": "Request body size pre-flight check for OpenAI-compatible provider", - "description": "Implement pre-flight request body size estimation to prevent 400 Bad Request errors from API gateways with size limits. Based on dogfood findings with kimi-k2.5 testing, DashScope API has a 6MB request body limit that was exceeded by large system prompts.", - "acceptanceCriteria": [ - "Pre-flight size estimation before sending requests to OpenAI-compatible providers", - "Clear error message when request exceeds provider-specific size limit", - "Configuration for different provider limits (6MB DashScope, 100MB OpenAI, etc.)", - "Unit tests for size estimation and limit checking", - "Integration with existing error handling for actionable user messages" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-022", - "title": "Enhanced error context for API failures", - "description": "Add structured error context to API failures including request ID tracking across retries, provider-specific error code mapping, and suggested user actions based on error type (e.g., 'Reduce prompt size' for 413, 'Check API key' for 401).", - "acceptanceCriteria": [ - "Request ID tracking across retries with full context in error messages", - "Provider-specific error code mapping with actionable suggestions", - "Suggested user actions for common error types (401, 403, 413, 429, 500, 502-504)", - "Unit tests for error context extraction", - "All existing tests pass and clippy is clean" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-023", - "title": "Add automatic routing for kimi models to DashScope", - "description": "Based on dogfood findings with kimi-k2.5 testing, users must manually prefix with dashscope/kimi-k2.5 instead of just using kimi-k2.5. Add automatic routing for kimi/ and kimi- prefixed models to DashScope (similar to qwen models), and add a 'kimi' alias to the model registry.", - "acceptanceCriteria": [ - "kimi/ and kimi- prefix routing to DashScope in metadata_for_model()", - "'kimi' alias in MODEL_REGISTRY that resolves to 'kimi-k2.5'", - "resolve_model_alias() handles the kimi alias correctly", - "Unit tests for kimi routing (similar to qwen routing tests)", - "All tests pass and clippy is clean" - ], - "passes": true, - "priority": "P1" - }, - { - "id": "US-024", - "title": "Add token limit metadata for kimi models", - "description": "The model_token_limit() function has no entries for kimi-k2.5 or kimi-k1.5, causing preflight context window validation to skip these models. Add token limit metadata to enable preflight checks and accurate max token defaults. Per Moonshot AI documentation, kimi-k2.5 supports 256K context window and 16K max output tokens.", - "acceptanceCriteria": [ - "model_token_limit('kimi-k2.5') returns Some(ModelTokenLimit { max_output_tokens: 16384, context_window_tokens: 256000 })", - "model_token_limit('kimi-k1.5') returns appropriate limits", - "model_token_limit('kimi') follows alias chain (kimi → kimi-k2.5) and returns k2.5 limits", - "preflight_message_request() validates context window for kimi models (via generic preflight, no provider-specific code needed)", - "Unit tests verify limits and preflight behavior for kimi models", - "All tests pass and clippy is clean" - ], - "passes": true, - "priority": "P1" - } - ], - "metadata": { - "lastUpdated": "2026-04-17", - "completedStories": ["US-001", "US-002", "US-003", "US-004", "US-005", "US-006", "US-007", "US-008", "US-009", "US-010", "US-011", "US-012", "US-013", "US-014", "US-015", "US-016", "US-017", "US-018", "US-019", "US-020", "US-021", "US-022", "US-023", "US-024"], - "inProgressStories": [], - "totalStories": 24, - "status": "completed" - } -} diff --git a/progress.txt b/progress.txt deleted file mode 100644 index d7953a17..00000000 --- a/progress.txt +++ /dev/null @@ -1,378 +0,0 @@ -Ralph Iteration Summary - claw-code Roadmap Implementation -=========================================================== - -Iteration 1: 2026-04-16 ------------------------- - -US-001 COMPLETED (Phase 1.6 - startup-no-evidence evidence bundle + classifier) -- Files: rust/crates/runtime/src/worker_boot.rs -- Added StartupFailureClassification enum with 6 variants -- Added StartupEvidenceBundle with 8 fields -- Implemented classify_startup_failure() logic -- Added observe_startup_timeout() method to Worker -- Tests: 6 new tests verifying classification logic - -US-002 COMPLETED (Phase 2 - Canonical lane event schema) -- Files: rust/crates/runtime/src/lane_events.rs -- Added EventProvenance enum with 5 labels -- Added SessionIdentity, LaneOwnership structs -- Added LaneEventMetadata with sequence/ordering -- Added LaneEventBuilder for construction -- Implemented is_terminal_event(), dedupe_terminal_events() -- Tests: 10 new tests for events and deduplication - -US-005 COMPLETED (Phase 4 - Typed task packet format) -- Files: - - rust/crates/runtime/src/task_packet.rs - - rust/crates/runtime/src/task_registry.rs - - rust/crates/tools/src/lib.rs -- Added TaskScope enum (Workspace, Module, SingleFile, Custom) -- Updated TaskPacket with scope_path and worktree fields -- Added validate_scope_requirements() validation logic -- Fixed all test compilation errors in dependent modules -- Tests: Updated existing tests to use new types - -PRE-EXISTING IMPLEMENTATIONS (verified working): ------------------------------------------------- - -US-003 COMPLETE (Phase 3 - Stale-branch detection) -- Files: rust/crates/runtime/src/stale_branch.rs -- BranchFreshness enum (Fresh, Stale, Diverged) -- StaleBranchPolicy (AutoRebase, AutoMergeForward, WarnOnly, Block) -- StaleBranchEvent with structured events -- check_freshness() with git integration -- apply_policy() with policy resolution -- Tests: 12 unit tests + 5 integration tests passing - -US-004 COMPLETE (Phase 3 - Recovery recipes with ledger) -- Files: rust/crates/runtime/src/recovery_recipes.rs -- FailureScenario enum with 7 scenarios -- RecoveryStep enum with actionable steps -- RecoveryRecipe with step sequences -- RecoveryLedger for attempt tracking -- RecoveryEvent for structured emission -- attempt_recovery() with escalation logic -- Tests: 15 unit tests + 1 integration test passing - -US-006 COMPLETE (Phase 4 - Policy engine for autonomous coding) -- Files: rust/crates/runtime/src/policy_engine.rs -- PolicyRule with condition/action/priority -- PolicyCondition (And, Or, GreenAt, StaleBranch, etc.) -- PolicyAction (MergeToDev, RecoverOnce, Escalate, etc.) -- LaneContext for evaluation context -- evaluate() for rule matching -- Tests: 18 unit tests + 6 integration tests passing - -US-007 COMPLETE (Phase 5 - Plugin/MCP lifecycle maturity) -- Files: rust/crates/runtime/src/plugin_lifecycle.rs -- ServerStatus enum (Healthy, Degraded, Failed) -- ServerHealth with capabilities tracking -- PluginState with full lifecycle states -- PluginLifecycle event tracking -- PluginHealthcheck structured results -- DiscoveryResult for capability discovery -- DegradedMode behavior -- Tests: 11 unit tests passing - - -Iteration 2026-04-27 - ROADMAP #200 COMPLETED ------------------------------------------------- -- Selected next actionable backlog item because no active task was in progress. -- ROADMAP #200: Interactive MCP/tool permission prompts are invisible blockers. -- Files: rust/crates/runtime/src/worker_boot.rs, rust/crates/runtime/src/recovery_recipes.rs, ROADMAP.md, progress.txt. -- Added tool_permission_required worker status and event classification for interactive MCP/tool permission gates. -- Added structured ToolPermissionPrompt payload with server/tool identity and prompt preview. -- Startup evidence now records tool_permission_prompt_detected and classifies timeout evidence as tool_permission_required. -- Readiness snapshots now mark tool-permission-gated workers as blocked, not ready/idle. -- Tests: targeted tool_permission regressions, full runtime test/clippy/fmt pending in Ralph verification loop. - -VERIFICATION STATUS: ------------------- -- cargo build --workspace: PASSED -- cargo test --workspace: PASSED (476+ unit tests, 12 integration tests) -- cargo clippy --workspace: PASSED - -All 7 stories from prd.json now have passes: true - -Iteration 2: 2026-04-16 ------------------------- - -US-009 COMPLETED (Add unit tests for kimi model compatibility fix) -- Files: rust/crates/api/src/providers/openai_compat.rs -- Added 4 comprehensive unit tests: - 1. model_rejects_is_error_field_detects_kimi_models - verifies detection of kimi-k2.5, kimi-k1.5, dashscope/kimi-k2.5, case insensitivity - 2. translate_message_includes_is_error_for_non_kimi_models - verifies gpt-4o, grok-3, claude include is_error - 3. translate_message_excludes_is_error_for_kimi_models - verifies kimi models exclude is_error (prevents 400 Bad Request) - 4. build_chat_completion_request_kimi_vs_non_kimi_tool_results - full integration test for request building -- Tests: 4 new tests, 119 unit tests total in api crate (+4), all passing -- Integration tests: 29 passing (no regressions) - -US-010 COMPLETED (Add model compatibility documentation) -- Files: docs/MODEL_COMPATIBILITY.md -- Created comprehensive documentation covering: - 1. Kimi Models (is_error Exclusion) - documents the 400 Bad Request issue and solution - 2. Reasoning Models (Tuning Parameter Stripping) - covers o1, o3, o4, grok-3-mini, qwen-qwq, qwen3-thinking - 3. GPT-5 (max_completion_tokens) - documents max_tokens vs max_completion_tokens requirement - 4. Qwen Models (DashScope Routing) - explains routing and authentication -- Added implementation details section with key functions -- Added "Adding New Models" guide for future contributors -- Added testing section with example commands -- Cross-referenced with existing code comments in openai_compat.rs -- cargo clippy passes - -Iteration 3: 2026-04-16 ------------------------- - -US-012 COMPLETED (Trust prompt resolver with allowlist auto-trust) -- Files: rust/crates/runtime/src/trust_resolver.rs -- Enhanced TrustConfig with pattern matching and serde support: - - TrustAllowlistEntry struct with pattern, worktree_pattern, description - - TrustResolution enum (AutoAllowlisted, ManualApproval) - - Enhanced TrustEvent variants with serde tags and metadata - - Glob pattern matching with * and ? wildcards - - Support for path prefix matching and worktree patterns -- Updated TrustResolver with new resolve() signature: - - Added worktree parameter for worktree pattern matching - - Proper event emission with TrustResolution - - Manual approval detection from screen text -- Added helper functions: - - extract_repo_name() - extracts repo name from path - - detect_manual_approval() - detects manual trust from screen text - - glob_matches() - recursive backtracking glob matcher -- Tests: 25 new tests for pattern matching, serialization, and resolver behavior -- All 483 runtime tests pass -- cargo clippy passes with no warnings - -US-011 COMPLETED (Performance optimization: reduce API request serialization overhead) -- Files: - - rust/crates/api/Cargo.toml (added criterion dev-dependency and bench config) - - rust/crates/api/benches/request_building.rs (new benchmark suite) - - rust/crates/api/src/providers/openai_compat.rs (optimizations) - - rust/crates/api/src/lib.rs (public exports for benchmarks) -- Optimizations implemented: - 1. flatten_tool_result_content: Pre-allocate String capacity and avoid intermediate Vec - - Before: collected to Vec then joined - - After: single String with pre-calculated capacity, push directly - 2. Made key functions public for benchmarking: translate_message, build_chat_completion_request, - flatten_tool_result_content, is_reasoning_model, model_rejects_is_error_field -- Benchmark results: - - flatten_tool_result_content/single_text: ~17ns - - flatten_tool_result_content/multi_text (10 blocks): ~46ns - - flatten_tool_result_content/large_content (50 blocks): ~11.7µs - - translate_message/text_only: ~200ns - - translate_message/tool_result: ~348ns - - build_chat_completion_request/10 messages: ~16.4µs - - build_chat_completion_request/100 messages: ~209µs - - is_reasoning_model detection: ~26-42ns depending on model -- All tests pass (119 unit tests + 29 integration tests) -- cargo clippy passes - -VERIFICATION STATUS (Iteration 3): ----------------------------------- -- cargo build --workspace: PASSED -- cargo test --workspace: PASSED (891+ tests) -- cargo clippy --workspace --all-targets -- -D warnings: PASSED -- cargo fmt -- --check: PASSED - -All 12 stories from prd.json now have passes: true -- US-001 through US-007: Pre-existing implementations -- US-008: kimi-k2.5 model API compatibility fix -- US-009: Unit tests for kimi model compatibility -- US-010: Model compatibility documentation -- US-011: Performance optimization with criterion benchmarks -- US-012: Trust prompt resolver with allowlist auto-trust - -Iteration 4: 2026-04-16 ------------------------- - -US-013 COMPLETED (Phase 2 - Session event ordering + terminal-state reconciliation) -- Files: rust/crates/runtime/src/lane_events.rs -- Added EventTerminality enum (Terminal, Advisory, Uncertainty) -- Added classify_event_terminality() function for event classification -- Added reconcile_terminal_events() function for deterministic event ordering: - - Sorts events by monotonic sequence number - - Deduplicates terminal events by fingerprint - - Detects transport death uncertainty (terminal + transport death) - - Handles out-of-order event bursts -- Added events_materially_differ() for detecting meaningful differences -- Added 8 comprehensive tests for reconciliation logic: - - reconcile_terminal_events_sorts_by_monotonic_sequence - - reconcile_terminal_events_deduplicates_same_fingerprint - - reconcile_terminal_events_detects_transport_death_uncertainty - - reconcile_terminal_events_handles_completed_idle_error_completed_noise - - reconcile_terminal_events_returns_none_for_empty_input - - reconcile_terminal_events_preserves_advisory_events - - events_materially_differ_detects_real_differences - - classify_event_terminality_correctly_classifies -- Fixed test compilation issues with LaneEventBuilder API - -VERIFICATION STATUS (Iteration 4): ----------------------------------- -- cargo build --workspace: PASSED -- cargo test --workspace: PASSED (891+ tests) -- cargo clippy --workspace --all-targets -- -D warnings: PASSED -- cargo fmt -- --check: PASSED - -US-013 marked passes: true in prd.json - -US-014 COMPLETED (Phase 2 - Event provenance / environment labeling) -- Files: rust/crates/runtime/src/lane_events.rs -- Added ConfidenceLevel enum (High, Medium, Low, Unknown) -- Added fields to LaneEventMetadata: - - environment_label: Option - environment/channel (production, staging, dev) - - emitter_identity: Option - emitter (clawd, plugin-name, operator-id) - - confidence_level: Option - trust level for automation -- Added builder methods: with_environment(), with_emitter(), with_confidence() -- Added filtering functions: - - filter_by_provenance() - select events by source - - filter_by_environment() - select events by environment label - - filter_by_confidence() - select events above confidence threshold - - is_test_event() - check if synthetic source (test, healthcheck, replay) - - is_live_lane_event() - check if production event -- Added 7 comprehensive tests for US-014: - - confidence_level_round_trips_through_serialization - - filter_by_provenance_selects_only_matching_events - - filter_by_environment_selects_only_matching_environment - - filter_by_confidence_selects_events_above_threshold - - is_test_event_detects_synthetic_sources - - is_live_lane_event_detects_production_events - - lane_event_metadata_includes_us014_fields - -US-016 COMPLETED (Phase 2 - Duplicate terminal-event suppression) -- Files: rust/crates/runtime/src/lane_events.rs -- Event fingerprinting already implemented via compute_event_fingerprint() -- Fingerprint attached via LaneEventMetadata.event_fingerprint -- Deduplication via dedupe_terminal_events() - returns first occurrence of each fingerprint -- Raw event history preserved separately from deduplicated actionable events -- Material difference detection via events_materially_differ(): - - Different event type (Finished vs Failed) is material - - Different status is material - - Different failure class is material - - Different data payload is material -- Reconcile function surfaces latest terminal event when materially different -- Added 5 comprehensive tests for US-016: - - canonical_terminal_event_fingerprint_attached_to_metadata - - dedupe_terminal_events_suppresses_repeated_fingerprints - - dedupe_preserves_raw_event_history_separately - - events_materially_differ_detects_payload_differences - - reconcile_terminal_events_surfaces_latest_when_different - -US-017 COMPLETED (Phase 2 - Lane ownership / scope binding) -- Files: rust/crates/runtime/src/lane_events.rs -- LaneOwnership struct already existed with: - - owner: String - owner/assignee identity - - workflow_scope: String - workflow scope (claw-code-dogfood, etc.) - - watcher_action: WatcherAction - Act, Observe, Ignore -- Ownership preserved through lifecycle via with_ownership() builder method -- All lifecycle events (Started -> Ready -> Finished) preserve ownership -- Added 3 comprehensive tests for US-017: - - lane_ownership_attached_to_metadata - - lane_ownership_preserved_through_lifecycle_events - - lane_ownership_watcher_action_variants - -US-015 COMPLETED (Phase 2 - Session identity completeness at creation time) -- Files: rust/crates/runtime/src/lane_events.rs -- SessionIdentity struct already existed with: - - title: String - stable title for the session - - workspace: String - workspace/worktree path - - purpose: String - lane/session purpose - - placeholder_reason: Option - reason for placeholder values -- Added reconcile_enriched() method for updating session identity: - - Updates title/workspace/purpose with newly available data - - Clears placeholder_reason when real values are provided - - Preserves existing values for fields not being updated - - Allows incremental enrichment without ambiguity -- Added 2 comprehensive tests: - - session_identity_reconcile_enriched_updates_fields - - session_identity_reconcile_preserves_placeholder_if_no_new_data - -US-018 COMPLETED (Phase 2 - Nudge acknowledgment / dedupe contract) -- Files: rust/crates/runtime/src/lane_events.rs -- Added NudgeTracking struct: - - nudge_id: String - unique nudge identifier - - delivered_at: String - timestamp of delivery - - acknowledged: bool - whether acknowledged - - acknowledged_at: Option - when acknowledged - - is_retry: bool - whether this is a retry - - original_nudge_id: Option - original ID if retry -- Added NudgeClassification enum (New, Retry, StaleDuplicate) -- Added classify_nudge() function for deduplication logic -- Added 6 comprehensive tests for US-018 - -US-019 COMPLETED (Phase 2 - Stable roadmap-id assignment) -- Files: rust/crates/runtime/src/lane_events.rs -- Added RoadmapId struct: - - id: String - canonical unique identifier - - filed_at: String - timestamp when filed - - is_new_filing: bool - new vs update - - supersedes: Option - lineage for supersedes -- Added builder methods: new_filing(), update(), supersedes() -- Added 3 comprehensive tests for US-019 - -US-020 COMPLETED (Phase 2 - Roadmap item lifecycle state contract) -- Files: rust/crates/runtime/src/lane_events.rs -- Added RoadmapLifecycleState enum (Filed, Acknowledged, InProgress, Blocked, Done, Superseded) -- Added RoadmapLifecycle struct: - - state: RoadmapLifecycleState - current state - - state_changed_at: String - last transition timestamp - - filed_at: String - original filing timestamp - - lineage: Vec - supersession chain -- Added methods: new_filed(), transition(), superseded_by(), is_terminal(), is_active() -- Added 5 comprehensive tests for US-020 - -VERIFICATION STATUS (Iteration 7): ----------------------------------- -- cargo build --workspace: PASSED -- cargo test --workspace: PASSED (891+ tests) -- cargo clippy --workspace --all-targets -- -D warnings: PASSED -- cargo fmt -- --check: PASSED - -US-013 through US-015 and US-018 through US-020 now marked passes: true - -FINAL VERIFICATION (All 20 Stories Complete): ------------------------------------------------- -- cargo build --workspace: PASSED -- cargo test --workspace: PASSED (119+ API tests, 39 runtime tests, 12 integration tests) -- cargo clippy --workspace --all-targets -- -D warnings: PASSED -- cargo fmt -- --check: PASSED - -ALL 20 STORIES FROM PRD COMPLETE: -- US-001 through US-012: Pre-existing implementations (verified working) -- US-013: Session event ordering + terminal-state reconciliation -- US-014: Event provenance / environment labeling -- US-015: Session identity completeness at creation time -- US-016: Duplicate terminal-event suppression -- US-017: Lane ownership / scope binding -- US-018: Nudge acknowledgment / dedupe contract -- US-019: Stable roadmap-id assignment -- US-020: Roadmap item lifecycle state contract - -Iteration 8: 2026-04-16 ------------------------- - -US-021 COMPLETED (Request body size pre-flight check - from dogfood findings) -- Files: - - rust/crates/api/src/error.rs (new error variant) - - rust/crates/api/src/providers/openai_compat.rs -- Added RequestBodySizeExceeded error variant with actionable message -- Added max_request_body_bytes to OpenAiCompatConfig: - - DashScope: 6MB (6_291_456 bytes) - from dogfood with kimi-k2.5 - - OpenAI: 100MB (104_857_600 bytes) - - xAI: 50MB (52_428_800 bytes) -- Added estimate_request_body_size() for pre-flight checks -- Added check_request_body_size() for validation -- Pre-flight check integrated in send_raw_request() -- Tests: 5 new tests for size estimation and limit checking - -PROJECT STATUS: COMPLETE (21/21 stories) - -Iteration 2026-04-29 - ROADMAP #96 COMPLETED ------------------------------------------------- -- Pulled origin/main: already up to date. -- Selected ROADMAP #96 as a small repo-local Immediate Backlog item: the `claw --help` Resume-safe command summary leaked slash-command stubs despite the main Interactive command listing filtering them. -- Files: rust/crates/rusty-claude-cli/src/main.rs, ROADMAP.md, progress.txt. -- Changed help rendering to filter `resume_supported_slash_commands()` through `STUB_COMMANDS` before building the Resume-safe one-liner. -- Added `stub_commands_absent_from_resume_safe_help` regression coverage so future stub additions cannot leak into the Resume-safe summary. -- Targeted verification: `cargo test -p rusty-claude-cli stub_commands_absent_from_resume_safe_help -- --nocapture` passed; `cargo test -p rusty-claude-cli parses_direct_cli_actions -- --nocapture` passed. -- Format/check verification: `cargo fmt --all --check`, `git diff --check`, and `cargo check -p rusty-claude-cli` passed. -- Broader clippy note: `cargo clippy -p rusty-claude-cli --all-targets -- -D warnings` is blocked by pre-existing `clippy::unnecessary_wraps` failures in `rust/crates/commands/src/lib.rs` (`render_mcp_report_for`, `render_mcp_report_json_for`), outside this diff. diff --git a/resolve_repl_model_falls_back_to_anthropic_model_env_when_default.rs_chunk b/resolve_repl_model_falls_back_to_anthropic_model_env_when_default.rs_chunk deleted file mode 100644 index 60eee673..00000000 --- a/resolve_repl_model_falls_back_to_anthropic_model_env_when_default.rs_chunk +++ /dev/null @@ -1,19 +0,0 @@ -#[test] - fn resolve_repl_model_falls_back_to_anthropic_model_env_when_default() { - let _guard = env_lock(); - let root = temp_dir(); - fs::create_dir_all(&root).expect("root dir"); - let config_home = root.join("config"); - fs::create_dir_all(&config_home).expect("config home dir"); - std::env::set_var("CLAW_CONFIG_HOME", &config_home); - std::env::remove_var("ANTHROPIC_MODEL"); - std::env::set_var("ANTHROPIC_MODEL", "sonnet"); - - let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string())); - - assert_eq!(resolved, "claude-sonnet-4-6"); - - std::env::remove_var("ANTHROPIC_MODEL"); - std::env::remove_var("CLAW_CONFIG_HOME"); - fs::remove_dir_all(root).expect("cleanup temp dir"); - } \ No newline at end of file diff --git a/resolve_repl_model_returns_default_when_env_unset_and_no_config.rs_chunk b/resolve_repl_model_returns_default_when_env_unset_and_no_config.rs_chunk deleted file mode 100644 index 7728cde5..00000000 --- a/resolve_repl_model_returns_default_when_env_unset_and_no_config.rs_chunk +++ /dev/null @@ -1,17 +0,0 @@ -#[test] - fn resolve_repl_model_returns_default_when_env_unset_and_no_config() { - let _guard = env_lock(); - let root = temp_dir(); - fs::create_dir_all(&root).expect("root dir"); - let config_home = root.join("config"); - fs::create_dir_all(&config_home).expect("config home dir"); - std::env::set_var("CLAW_CONFIG_HOME", &config_home); - std::env::remove_var("ANTHROPIC_MODEL"); - - let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string())); - - assert_eq!(resolved, DEFAULT_MODEL); - - std::env::remove_var("CLAW_CONFIG_HOME"); - fs::remove_dir_all(root).expect("cleanup temp dir"); - } \ No newline at end of file diff --git a/resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit.rs_chunk b/resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit.rs_chunk deleted file mode 100644 index bb12d14d..00000000 --- a/resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit.rs_chunk +++ /dev/null @@ -1,8 +0,0 @@ -#[test] - fn resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit() { - let user_model = "claude-sonnet-4-6".to_string(); - - let resolved = resolve_repl_model(user_model); - - assert_eq!(resolved, "claude-sonnet-4-6"); - } \ No newline at end of file diff --git a/resolves_known_model_aliases.rs_chunk b/resolves_known_model_aliases.rs_chunk deleted file mode 100644 index a532303c..00000000 --- a/resolves_known_model_aliases.rs_chunk +++ /dev/null @@ -1,7 +0,0 @@ -#[test] - fn resolves_known_model_aliases() { - assert_eq!(resolve_model_alias("opus"), "claude-opus-4-6"); - assert_eq!(resolve_model_alias("sonnet"), "claude-sonnet-4-6"); - assert_eq!(resolve_model_alias("haiku"), "claude-haiku-4-5-20251213"); - assert_eq!(resolve_model_alias("claude-opus"), "claude-opus"); - } \ No newline at end of file diff --git a/rm_from_main.py b/rm_from_main.py deleted file mode 100644 index 1be8a216..00000000 --- a/rm_from_main.py +++ /dev/null @@ -1,14 +0,0 @@ -import re - -MAIN_FILE = "rust/crates/rusty-claude-cli/src/main.rs" - -with open(MAIN_FILE, 'r') as f: - content = f.read() - -# remove AnthropicRuntimeClient from main.rs -content = re.sub(r'struct AnthropicRuntimeClient \{.*?\n\}\n\nimpl AnthropicRuntimeClient \{.*?\n\}\n\nimpl runtime::Client for AnthropicRuntimeClient \{.*?\n\}', '', content, flags=re.DOTALL | re.MULTILINE) - -with open(MAIN_FILE, 'w') as f: - f.write(content) - -print("Removed from main.rs") diff --git a/user_defined_aliases_resolve_before_provider_dispatch.rs_chunk b/user_defined_aliases_resolve_before_provider_dispatch.rs_chunk deleted file mode 100644 index 8bcd5f8d..00000000 --- a/user_defined_aliases_resolve_before_provider_dispatch.rs_chunk +++ /dev/null @@ -1,38 +0,0 @@ -#[test] - fn user_defined_aliases_resolve_before_provider_dispatch() { - // given - let _guard = env_lock(); - let root = temp_dir(); - let cwd = root.join("project"); - let config_home = root.join("config-home"); - std::fs::create_dir_all(cwd.join(".claw")).expect("project config dir should exist"); - std::fs::create_dir_all(&config_home).expect("config home should exist"); - std::fs::write( - cwd.join(".claw").join("settings.json"), - r#"{"aliases":{"fast":"claude-haiku-4-5-20251213","smart":"opus","cheap":"grok-3-mini"}}"#, - ) - .expect("project config should write"); - - let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok(); - std::env::set_var("CLAW_CONFIG_HOME", &config_home); - - // when - let direct = with_current_dir(&cwd, || resolve_model_alias_with_config("fast")); - let chained = with_current_dir(&cwd, || resolve_model_alias_with_config("smart")); - let cross_provider = with_current_dir(&cwd, || resolve_model_alias_with_config("cheap")); - let unknown = with_current_dir(&cwd, || resolve_model_alias_with_config("unknown-model")); - let builtin = with_current_dir(&cwd, || resolve_model_alias_with_config("haiku")); - - match original_config_home { - Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value), - None => std::env::remove_var("CLAW_CONFIG_HOME"), - } - std::fs::remove_dir_all(root).expect("temp config root should clean up"); - - // then - assert_eq!(direct, "claude-haiku-4-5-20251213"); - assert_eq!(chained, "claude-opus-4-6"); - assert_eq!(cross_provider, "grok-3-mini"); - assert_eq!(unknown, "unknown-model"); - assert_eq!(builtin, "claude-haiku-4-5-20251213"); - } \ No newline at end of file