chore: 清理临时文件
This commit is contained in:
parent
77651f089b
commit
d5021287d7
210
README-old.md
210
README-old.md
|
|
@ -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 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**.
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
}
|
||||
|
|
@ -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"))
|
||||
})
|
||||
}
|
||||
}
|
||||
19
debug.sh
19
debug.sh
|
|
@ -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 "帮我看看这个项目的内容"
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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}")
|
||||
|
||||
40
fix_all.py
40
fix_all.py
|
|
@ -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.")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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.")
|
||||
114
move_tests.py
114
move_tests.py
|
|
@ -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.")
|
||||
|
|
@ -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);
|
||||
}
|
||||
356
prd.json
356
prd.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
378
progress.txt
378
progress.txt
|
|
@ -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.
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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");
|
||||
}
|
||||
Loading…
Reference in New Issue