chore: 清理临时文件

This commit is contained in:
zhaoyanchao 2026-06-23 20:24:26 +08:00
parent 77651f089b
commit d5021287d7
30 changed files with 0 additions and 2176 deletions

View File

@ -1,210 +0,0 @@
# Claw Code
<p align="center">
<a href="https://github.com/ultraworkers/claw-code">ultraworkers/claw-code</a>
·
<a href="./USAGE.md">Usage</a>
·
<a href="./rust/README.md">Rust workspace</a>
·
<a href="./PARITY.md">Parity</a>
·
<a href="./ROADMAP.md">Roadmap</a>
·
<a href="https://discord.gg/5TUQKqFWd">UltraWorkers Discord</a>
</p>
<p align="center">
<a href="https://star-history.com/#ultraworkers/claw-code&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=ultraworkers/claw-code&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=ultraworkers/claw-code&type=Date" />
<img alt="Star history for ultraworkers/claw-code" src="https://api.star-history.com/svg?repos=ultraworkers/claw-code&type=Date" width="600" />
</picture>
</a>
</p>
<p align="center">
<img src="assets/claw-hero.jpeg" alt="Claw Code" width="300" />
</p>
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 <https://rustup.rs/> 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 510 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**.

View File

@ -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.")

View File

@ -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<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
progress_reporter: Option<InternalPromptProgressReporter>,
reasoning_effort: Option<String>,
}
impl AnthropicRuntimeClient {
pub(crate) fn new(
session_id: &str,
model: String,
enable_tools: bool,
emit_output: bool,
allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
progress_reporter: Option<InternalPromptProgressReporter>,
) -> Result<Self, Box<dyn std::error::Error>> {
// 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<String>) {
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<Vec<AssistantEvent>, 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)
}
}

View File

@ -1,243 +0,0 @@
impl AnthropicRuntimeClient {
fn new(
session_id: &str,
model: String,
enable_tools: bool,
emit_output: bool,
allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
progress_reporter: Option<InternalPromptProgressReporter>,
) -> Result<Self, Box<dyn std::error::Error>> {
// 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<String>) {
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<Vec<AssistantEvent>, 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)
}
}

View File

@ -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<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
progress_reporter: Option<InternalPromptProgressReporter>,
reasoning_effort: Option<String>,
}

View File

@ -1,49 +0,0 @@
impl ApiClient for AnthropicRuntimeClient {
#[allow(clippy::too_many_lines)]
fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, 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"))
})
}
}

View File

@ -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 "帮我看看这个项目的内容"

View File

@ -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"
);
}

View File

@ -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<String>,\n enable_tools: bool,\n emit_output: bool,\n allowed_tools: Option<AllowedToolSet>,\n permission_mode: PermissionMode,\n reasoning_effort: Option<String>,\n\) -> Result<Runtime, Box<dyn std::error::Error>> \{.*?\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<String>,\n enable_tools: bool,\n emit_output: bool,\n allowed_tools: Option<AllowedToolSet>,\n permission_mode: PermissionMode,\n reasoning_effort: Option<String>,\n plugin_state: RuntimePluginState,\n\) -> Result<Runtime, Box<dyn std::error::Error>> \{.*?\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<RuntimePluginState, Box<dyn std::error::Error>> \{.*?\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<RuntimePluginState, Box<dyn std::error::Error>> \{.*?\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<Arc<Mutex<RuntimeMcpState>>> \{.*?\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<RuntimeToolDefinition> \{.*?\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")

View File

@ -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")

View File

@ -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<Vec<AssistantEvent>, 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<Vec<AssistantEvent>, 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<AssistantEvent>\) \{.*?\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<PromptCacheEvent> \{.*?\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")

View File

@ -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}")

View File

@ -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.")

View File

@ -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")

View File

@ -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

View File

@ -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<Mutex<()>> = 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<Mutex<()>> = 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<T>(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)

View File

@ -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

View File

@ -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")

View File

@ -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")

View File

@ -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.")

View File

@ -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.")

View File

@ -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,
&registry_with_plugin_tool(),
)
.expect("permission policy should build");
let required = policy.required_mode_for("plugin_echo");
assert_eq!(required, PermissionMode::WorkspaceWrite);
}

356
prd.json
View File

@ -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"
}
}

View File

@ -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<String> 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<String> - environment/channel (production, staging, dev)
- emitter_identity: Option<String> - emitter (clawd, plugin-name, operator-id)
- confidence_level: Option<ConfidenceLevel> - 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<String> - 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<String> - when acknowledged
- is_retry: bool - whether this is a retry
- original_nudge_id: Option<String> - 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<String> - 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<String> - 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.

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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")

View File

@ -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");
}