mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-09 17:44:50 +08:00
Compare commits
32 Commits
fix/linux-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54269da157 | ||
|
|
f741a42507 | ||
|
|
6b3e2d8854 | ||
|
|
1a8f73da01 | ||
|
|
7d9f11b91f | ||
|
|
8e1bca6b99 | ||
|
|
8d0308eecb | ||
|
|
4d10caebc6 | ||
|
|
414526c1bd | ||
|
|
2a2e205414 | ||
|
|
c55c510883 | ||
|
|
3fe0caf348 | ||
|
|
47086c1c14 | ||
|
|
e579902782 | ||
|
|
ca8950c26b | ||
|
|
b1d76983d2 | ||
|
|
c1b1ce465e | ||
|
|
8e25611064 | ||
|
|
eb044f0a02 | ||
|
|
75476c9005 | ||
|
|
e4c3871882 | ||
|
|
beb09df4b8 | ||
|
|
811b7b4c24 | ||
|
|
8a9300ea96 | ||
|
|
e7e0fd2dbf | ||
|
|
da451c66db | ||
|
|
ad38032ab8 | ||
|
|
7173f2d6c6 | ||
|
|
a0b4156174 | ||
|
|
3bf45fc44a | ||
|
|
af58b6a7c7 | ||
|
|
514c3da7ad |
29
README.md
29
README.md
@@ -45,22 +45,31 @@ The canonical implementation lives in [`rust/`](./rust), and the current source
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **`cargo install clawcode` will not work** — this package is not published on crates.io. Build from source as shown below.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd rust
|
# 1. Clone and build
|
||||||
|
git clone https://github.com/ultraworkers/claw-code
|
||||||
|
cd claw-code/rust
|
||||||
cargo build --workspace
|
cargo build --workspace
|
||||||
./target/debug/claw --help
|
|
||||||
./target/debug/claw prompt "summarize this repository"
|
|
||||||
```
|
|
||||||
|
|
||||||
Authenticate with either an API key or the built-in OAuth flow:
|
# 2. Set your API key (Anthropic API key — not a Claude subscription)
|
||||||
|
|
||||||
```bash
|
|
||||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||||
# or
|
|
||||||
cd rust
|
# 3. Verify everything is wired correctly
|
||||||
./target/debug/claw login
|
./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.
|
||||||
|
|
||||||
|
> [!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:
|
Run the workspace test suite:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
28
ROADMAP.md
28
ROADMAP.md
@@ -483,8 +483,32 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
|||||||
|
|
||||||
31. **`code-on-disk → verified commit lands` depends on undocumented executor quirks** — dogfooded 2026-04-08 during live fix session. Three hidden contracts tripped the "last mile" path when using droid via acpx in the claw-code workspace: **(a) hidden CWD contract** — droid's `terminal/create` rejects `cd /path && cargo build` compound commands with `spawn ENOENT`; callers must pass `--cwd` or split commands; **(b) hidden commit-message transport limit** — embedding a multi-line commit message in a single shell invocation hits `ENAMETOOLONG`; workaround is `git commit -F <file>` but the caller must know to write the file first; **(c) hidden workspace lint/edition contract** — `unsafe_code = "forbid"` workspace-wide with Rust 2021 edition makes `unsafe {}` wrappers incorrect for `set_var`/`remove_var`, but droid generates Rust 2024-style unsafe blocks without inspecting the workspace Cargo.toml or clippy config. Each of these required the orchestrator to learn the constraint by failing, then switching strategies. **Acceptance bar:** a fresh agent should be able to verify/commit/push a correct diff in this workspace without needing to know executor-specific shell trivia ahead of time. **Fix shape:** (1) `run-acpx.sh`-style wrapper that normalizes the commit idiom (always writes to temp file, sets `--cwd`, splits compound commands); (2) inject workspace constraints into the droid/acpx task preamble (edition, lint gates, known shell executor quirks) so the model doesn't have to discover them from failures; (3) or upstream a fix to the executor itself so `cd /path && cmd` chains work correctly.
|
31. **`code-on-disk → verified commit lands` depends on undocumented executor quirks** — dogfooded 2026-04-08 during live fix session. Three hidden contracts tripped the "last mile" path when using droid via acpx in the claw-code workspace: **(a) hidden CWD contract** — droid's `terminal/create` rejects `cd /path && cargo build` compound commands with `spawn ENOENT`; callers must pass `--cwd` or split commands; **(b) hidden commit-message transport limit** — embedding a multi-line commit message in a single shell invocation hits `ENAMETOOLONG`; workaround is `git commit -F <file>` but the caller must know to write the file first; **(c) hidden workspace lint/edition contract** — `unsafe_code = "forbid"` workspace-wide with Rust 2021 edition makes `unsafe {}` wrappers incorrect for `set_var`/`remove_var`, but droid generates Rust 2024-style unsafe blocks without inspecting the workspace Cargo.toml or clippy config. Each of these required the orchestrator to learn the constraint by failing, then switching strategies. **Acceptance bar:** a fresh agent should be able to verify/commit/push a correct diff in this workspace without needing to know executor-specific shell trivia ahead of time. **Fix shape:** (1) `run-acpx.sh`-style wrapper that normalizes the commit idiom (always writes to temp file, sets `--cwd`, splits compound commands); (2) inject workspace constraints into the droid/acpx task preamble (edition, lint gates, known shell executor quirks) so the model doesn't have to discover them from failures; (3) or upstream a fix to the executor itself so `cd /path && cmd` chains work correctly.
|
||||||
|
|
||||||
32. **OpenAI-compatible provider/model-id passthrough is not fully literal** — dogfooded 2026-04-08 via live user in #claw-code who confirmed the exact backend model id works outside claw but fails through claw for an OpenAI-compatible endpoint. The gap: `openai/` prefix is correctly used for **transport selection** (pick the OpenAI-compat client) but the **wire model id** — the string placed in `"model": "..."` in the JSON request body — may not be the literal backend model string the user supplied. Two candidate failure modes: **(a)** `resolve_model_alias()` is called on the model string before it reaches the wire — alias expansion designed for Anthropic/known models corrupts a user-supplied backend-specific id; **(b)** the `openai/` routing prefix may not be stripped before `build_chat_completion_request` packages the body, so backends receive `openai/gpt-4` instead of `gpt-4`. **Fix shape:** cleanly separate transport selection from wire model id. Transport selection uses the prefix; wire model id is the user-supplied string minus only the routing prefix — no alias expansion, no prefix leakage. **Trace path for next session:** (1) find where `resolve_model_alias()` is called relative to the OpenAI-compat dispatch path; (2) inspect what `build_chat_completion_request` puts in `"model"` for an `openai/some-backend-id` input. **Source:** live user in #claw-code 2026-04-08, confirmed exact model id works outside claw, fails through claw for OpenAI-compat backend.
|
32. **OpenAI-compatible provider/model-id passthrough is not fully literal** — **verified no-bug on 2026-04-09**: `resolve_model_alias()` only matches bare shorthand aliases (`opus`/`sonnet`/`haiku`) and passes everything else through unchanged, so `openai/gpt-4` reaches the dispatch layer unmodified. `strip_routing_prefix()` at `openai_compat.rs:732` then strips only recognised routing prefixes (`openai`, `xai`, `grok`, `qwen`) so the wire model is the bare backend id. No fix needed. **Original filing below.**
|
||||||
|
|
||||||
32. **OpenAI-compatible provider/model-id passthrough is not fully literal** — dogfooded 2026-04-08 via live user in #claw-code who confirmed the exact backend model id works outside claw but fails through claw for an OpenAI-compatible endpoint. The gap: `openai/` prefix is correctly used for **transport selection** (pick the OpenAI-compat client) but the **wire model id** — the string placed in `"model": "..."` in the JSON request body — may not be the literal backend model string the user supplied. Two candidate failure modes: **(a)** `resolve_model_alias()` is called on the model string before it reaches the wire — alias expansion designed for Anthropic/known models corrupts a user-supplied backend-specific id; **(b)** the `openai/` routing prefix may not be stripped before `build_chat_completion_request` packages the body, so backends receive `openai/gpt-4` instead of `gpt-4`. **Fix shape:** cleanly separate transport selection from wire model id. Transport selection uses the prefix; wire model id is the user-supplied string minus only the routing prefix — no alias expansion, no prefix leakage. **Trace path for next session:** (1) find where `resolve_model_alias()` is called relative to the OpenAI-compat dispatch path; (2) inspect what `build_chat_completion_request` puts in `"model"` for an `openai/some-backend-id` input. **Source:** live user in #claw-code 2026-04-08, confirmed exact model id works outside claw, fails through claw for OpenAI-compat backend.
|
32. **OpenAI-compatible provider/model-id passthrough is not fully literal** — dogfooded 2026-04-08 via live user in #claw-code who confirmed the exact backend model id works outside claw but fails through claw for an OpenAI-compatible endpoint. The gap: `openai/` prefix is correctly used for **transport selection** (pick the OpenAI-compat client) but the **wire model id** — the string placed in `"model": "..."` in the JSON request body — may not be the literal backend model string the user supplied. Two candidate failure modes: **(a)** `resolve_model_alias()` is called on the model string before it reaches the wire — alias expansion designed for Anthropic/known models corrupts a user-supplied backend-specific id; **(b)** the `openai/` routing prefix may not be stripped before `build_chat_completion_request` packages the body, so backends receive `openai/gpt-4` instead of `gpt-4`. **Fix shape:** cleanly separate transport selection from wire model id. Transport selection uses the prefix; wire model id is the user-supplied string minus only the routing prefix — no alias expansion, no prefix leakage. **Trace path for next session:** (1) find where `resolve_model_alias()` is called relative to the OpenAI-compat dispatch path; (2) inspect what `build_chat_completion_request` puts in `"model"` for an `openai/some-backend-id` input. **Source:** live user in #claw-code 2026-04-08, confirmed exact model id works outside claw, fails through claw for OpenAI-compat backend.
|
||||||
|
|
||||||
32. **OpenAI-compatible provider/model-id passthrough is not fully literal** — dogfooded 2026-04-08 via live user in #claw-code who confirmed the exact backend model id works outside claw but fails through claw for an OpenAI-compatible endpoint. The gap: `openai/` prefix is correctly used for **transport selection** (pick the OpenAI-compat client) but the **wire model id** — the string placed in `"model": "..."` in the JSON request body — may not be the literal backend model string the user supplied. Two candidate failure modes: **(a)** `resolve_model_alias()` is called on the model string before it reaches the wire — alias expansion designed for Anthropic/known models corrupts a user-supplied backend-specific id; **(b)** the `openai/` routing prefix may not be stripped before `build_chat_completion_request` packages the body, so backends receive `openai/gpt-4` instead of `gpt-4`. **Fix shape:** cleanly separate transport selection from wire model id. Transport selection uses the prefix; wire model id is the user-supplied string minus only the routing prefix — no alias expansion, no prefix leakage. **Trace path for next session:** (1) find where `resolve_model_alias()` is called relative to the OpenAI-compat dispatch path; (2) inspect what `build_chat_completion_request` puts in `"model"` for an `openai/some-backend-id` input. **Source:** live user in #claw-code 2026-04-08, confirmed exact model id works outside claw, fails through claw for OpenAI-compat backend.
|
33. **OpenAI `/responses` endpoint rejects claw's tool schema: `object schema missing properties` / `invalid_function_parameters`** — **done at `e7e0fd2` on 2026-04-09**. Added `normalize_object_schema()` in `openai_compat.rs` which recursively walks JSON Schema trees and injects `"properties": {}` and `"additionalProperties": false` on every object-type node (without overwriting existing values). Called from `openai_tool_definition()` so both `/chat/completions` and `/responses` receive strict-validator-safe schemas. 3 unit tests added. All api tests pass. **Original filing below.**
|
||||||
|
33. **OpenAI `/responses` endpoint rejects claw's tool schema: `object schema missing properties` / `invalid_function_parameters`** — dogfooded 2026-04-08 via live user in #claw-code. Repro: startup succeeds, provider routing succeeds (`Connected: gpt-5.4 via openai`), but request fails when claw sends tool/function schema to a `/responses`-compatible OpenAI backend. Backend rejects `StructuredOutput` with `object schema missing properties` and `invalid_function_parameters`. This is distinct from the `#32` model-id passthrough issue — routing and transport work correctly. The failure is at the schema validation layer: claw's tool schema is acceptable for `/chat/completions` but not strict enough for `/responses` endpoint validation. **Sharp next check:** emit what schema claw sends for `StructuredOutput` tool functions, compare against OpenAI `/responses` spec for strict JSON schema validation (required `properties` object, `additionalProperties: false`, etc). Likely fix: add missing `properties: {}` on object types, ensure `additionalProperties: false` is present on all object schemas in the function tool JSON. **Source:** live user in #claw-code 2026-04-08 with `gpt-5.4` on OpenAI-compat backend.
|
||||||
|
|
||||||
|
|
||||||
|
34. **`reasoning_effort` / `budget_tokens` not surfaced on OpenAI-compat path** — dogfooded 2026-04-09. Users asking for "reasoning effort parity with opencode" are hitting a structural gap: `MessageRequest` in `rust/crates/api/src/types.rs` has no `reasoning_effort` or `budget_tokens` field, and `build_chat_completion_request` in `openai_compat.rs` does not inject either into the request body. This means passing `--thinking` or equivalent to an OpenAI-compat reasoning model (e.g. `o4-mini`, `deepseek-r1`, any model that accepts `reasoning_effort`) silently drops the field — the model runs without the requested effort level, and the user gets no warning. **Contrast with Anthropic path:** `anthropic.rs` already maps `thinking` config into `anthropic.thinking.budget_tokens` in the request body. **Fix shape:** (a) Add optional `reasoning_effort: Option<String>` field to `MessageRequest`; (b) In `build_chat_completion_request`, if `reasoning_effort` is `Some`, emit `"reasoning_effort": value` in the JSON body; (c) In the CLI, wire `--thinking low/medium/high` or equivalent to populate the field when the resolved provider is `ProviderKind::OpenAi`; (d) Add unit test asserting `reasoning_effort` appears in the request body when set. **Source:** live user questions in #claw-code 2026-04-08/09 (dan_theman369 asking for "same flow as opencode for reasoning effort"; gaebal-gajae confirmed gap at `1491453913100976339`). Companion gap to #33 on the OpenAI-compat path.
|
||||||
|
|
||||||
|
35. **OpenAI gpt-5.x requires max_completion_tokens not max_tokens** -- dogfooded 2026-04-09. rklehm repro: gpt-5.2 via OpenAI-compat, startup OK, routing OK, but requests fail because claw emits max_tokens where gpt-5* requires max_completion_tokens. Fix: emit max_completion_tokens on OpenAI-compat path (backward-compatible). Add unit test. Source: rklehm in #claw-code 2026-04-09.
|
||||||
|
|
||||||
|
36. **Custom/project skill invocation disconnected from skill discovery** -- dogfooded 2026-04-09. /skills lists custom skills (e.g. caveman) but bare skill-name invocation does not dispatch them; falls through to plain model prompt. Fix: audit classify_skills_slash_command, ensure any skill listed by /skills has a deterministic invocation path, or document the correct syntax. Source: gaebal-gajae dogfood 2026-04-09.
|
||||||
|
|
||||||
|
37. **Claude subscription login path should be removed, not deprecated** -- dogfooded 2026-04-09. Official auth should be API key only (ANTHROPIC_API_KEY or OAuth access token via ANTHROPIC_AUTH_TOKEN). claw login with Claude subscription credentials creates legal/billing ambiguity. Fix: remove the subscription login surface entirely; update README/USAGE.md to say API key is the only supported path. Source: gaebal-gajae policy decision 2026-04-09.
|
||||||
|
|
||||||
|
38. **Dead-session opacity: bot cannot self-detect compaction vs broken tool surface** -- dogfooded 2026-04-09. Jobdori session spent ~15h declaring itself "dead" in-channel while tools were actually returning correct results within each turn. Root cause: context compaction causes tool outputs to be summarised away between turns, making the bot interpret absence-of-remembered-output as tool failure. This is a distinct failure mode from ROADMAP #31 (executor quirks): the session is alive and tools are functional, but the agent cannot tell the difference between "my last tool call produced no output" (compaction) and "the tool is broken". Downstream: repetitive false-dead signals in the channel, work not getting done despite the execution surface being live. Fix shape: (a) probe with a short known-output command at turn start if context has been compacted; (b) gate "I am dead" declarations behind at least one within-turn tool call with a verified non-empty result; (c) consider adding a session-health canary cron that fires a wake with a minimal probe and checks the result. Source: Jobdori self-dogfood 2026-04-09; observed in #clawcode-building-in-public across multiple Clawhip nudge cycles.
|
||||||
|
|
||||||
|
39. **Several slash commands are registered but not implemented: /branch, /rewind, /ide, /tag, /output-style, /add-dir** -- dogfooded 2026-04-09. These commands appear in the REPL completions surface but silently print 'Command registered but not yet implemented.' and return false. Users (mezz2301 in #claw-code) hit this as 'many features are not supported in this version now'. Fix shape: either (a) implement the missing commands, or (b) remove them from completions/help output until they are ready, so the discovery surface matches what actually works. Source: mezz2301 in #claw-code 2026-04-09; pinpointed in main.rs:3728.
|
||||||
|
|
||||||
|
40. **Surface broken installed plugins before they become support ghosts** — community-support lane. Clawhip commit `ff6d3b7` on worktree `claw-code-community-support-plugin-list-load-failures` / branch `community-support/plugin-list-load-failures`. When an installed plugin has a broken manifest (missing hook scripts, parse errors, bad json), the plugin silently fails to load and the user sees nothing — no warning, no list entry, no hint. Related to ROADMAP #27 (host plugin path leaking into tests) but at the user-facing surface: the test gap and the UX gap are siblings of the same root. Landing on `main` will close the silent-ghost class of support issues where users report "my plugin does nothing" with no error to share. Track until merged to `main`.
|
||||||
|
|
||||||
|
40. **Surface broken installed plugins before they become support ghosts** — community-support lane. Clawhip commit `ff6d3b7` on worktree `claw-code-community-support-plugin-list-load-failures` / branch `community-support/plugin-list-load-failures`. When an installed plugin has a broken manifest (missing hook scripts, parse errors, bad json), the plugin silently fails to load and the user sees nothing — no warning, no list entry, no hint. Related to ROADMAP #27 (host plugin path leaking into tests) but at the user-facing surface: the test gap and the UX gap are siblings of the same root. Landing on `main` will close the silent-ghost class of support issues where users report "my plugin does nothing" with no error to share. Track until merged to `main`.
|
||||||
|
|
||||||
|
41. **Stop ambient plugin state from skewing CLI regression checks** — community-support lane. Clawhip commit `7d493a7` on worktree `claw-code-community-support-plugin-test-sealing` / branch `community-support/plugin-test-sealing`. Companion to #40: the test sealing gap is the CI/developer side of the same root — host `~/.claude/plugins/installed/` bleeds into CLI test runs, making regression checks non-deterministic on any machine with a non-pristine plugin install. Closely related to ROADMAP #27 (dev/rust `cargo test` reads host plugin state). Track until merged to `main`.
|
||||||
|
|
||||||
|
42. **`--output-format json` errors emitted as prose, not JSON** — dogfooded 2026-04-09. When `claw --output-format json prompt` hits an API error, the error was printed as plain text (`error: api returned 401 ...`) to stderr instead of a JSON object. Any tool or CI step parsing claw's JSON output gets nothing parseable on failure — the error is invisible to the consumer. **Fix (`a...`):** detect `--output-format json` in `main()` at process exit and emit `{"type":"error","error":"<message>"}` to stderr instead of the prose format. Non-JSON path unchanged. **Done** in this nudge cycle.
|
||||||
|
|
||||||
|
43. **Hook ingress opacity: typed hook-health/delivery report missing** — dogfooded 2026-04-09 while wiring the agentika timer→hook→session bridge. Debugging hook delivery required manual HTTP probing and inferring state from raw status codes (404 = no route, 405 = route exists, 400 = body missing required field). No typed endpoint exists to report: route present/absent, accepted methods, mapping matched/not matched, target session resolved/not resolved, last delivery failure class. Fix shape: add `GET /hooks/health` (or `/hooks/status`) returning a structured JSON diagnostic — no auth exposure, just routing/matching/session state. Source: gaebal-gajae dogfood 2026-04-09.
|
||||||
|
|||||||
@@ -726,6 +726,24 @@ fn is_reasoning_model(model: &str) -> bool {
|
|||||||
|| canonical.contains("thinking")
|
|| canonical.contains("thinking")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
|
||||||
|
/// The prefix is used only to select transport; the backend expects the
|
||||||
|
/// bare model id.
|
||||||
|
fn strip_routing_prefix(model: &str) -> &str {
|
||||||
|
if let Some(pos) = model.find('/') {
|
||||||
|
let prefix = &model[..pos];
|
||||||
|
// Only strip if the prefix before "/" is a known routing prefix,
|
||||||
|
// not if "/" appears in the middle of the model name for other reasons.
|
||||||
|
if matches!(prefix, "openai" | "xai" | "grok" | "qwen") {
|
||||||
|
&model[pos + 1..]
|
||||||
|
} else {
|
||||||
|
model
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_chat_completion_request(request: &MessageRequest, config: OpenAiCompatConfig) -> Value {
|
fn build_chat_completion_request(request: &MessageRequest, config: OpenAiCompatConfig) -> Value {
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
if let Some(system) = request.system.as_ref().filter(|value| !value.is_empty()) {
|
if let Some(system) = request.system.as_ref().filter(|value| !value.is_empty()) {
|
||||||
@@ -738,9 +756,21 @@ fn build_chat_completion_request(request: &MessageRequest, config: OpenAiCompatC
|
|||||||
messages.extend(translate_message(message));
|
messages.extend(translate_message(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
|
||||||
|
let wire_model = strip_routing_prefix(&request.model);
|
||||||
|
|
||||||
|
// gpt-5* requires `max_completion_tokens`; older OpenAI models accept both.
|
||||||
|
// We send the correct field based on the wire model name so gpt-5.x requests
|
||||||
|
// don't fail with "unknown field max_tokens".
|
||||||
|
let max_tokens_key = if wire_model.starts_with("gpt-5") {
|
||||||
|
"max_completion_tokens"
|
||||||
|
} else {
|
||||||
|
"max_tokens"
|
||||||
|
};
|
||||||
|
|
||||||
let mut payload = json!({
|
let mut payload = json!({
|
||||||
"model": request.model,
|
"model": wire_model,
|
||||||
"max_tokens": request.max_tokens,
|
max_tokens_key: request.max_tokens,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"stream": request.stream,
|
"stream": request.stream,
|
||||||
});
|
});
|
||||||
@@ -780,6 +810,10 @@ fn build_chat_completion_request(request: &MessageRequest, config: OpenAiCompatC
|
|||||||
payload["stop"] = json!(stop);
|
payload["stop"] = json!(stop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// reasoning_effort for OpenAI-compatible reasoning models (o4-mini, o3, etc.)
|
||||||
|
if let Some(effort) = &request.reasoning_effort {
|
||||||
|
payload["reasoning_effort"] = json!(effort);
|
||||||
|
}
|
||||||
|
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
@@ -848,13 +882,45 @@ fn flatten_tool_result_content(content: &[ToolResultContentBlock]) -> String {
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively ensure every object-type node in a JSON Schema has
|
||||||
|
/// `"properties"` (at least `{}`) and `"additionalProperties": false`.
|
||||||
|
/// The OpenAI `/responses` endpoint validates schemas strictly and rejects
|
||||||
|
/// objects that omit these fields; `/chat/completions` is lenient but also
|
||||||
|
/// accepts them, so we normalise unconditionally.
|
||||||
|
fn normalize_object_schema(schema: &mut Value) {
|
||||||
|
if let Some(obj) = schema.as_object_mut() {
|
||||||
|
if obj.get("type").and_then(Value::as_str) == Some("object") {
|
||||||
|
obj.entry("properties").or_insert_with(|| json!({}));
|
||||||
|
obj.entry("additionalProperties")
|
||||||
|
.or_insert(Value::Bool(false));
|
||||||
|
}
|
||||||
|
// Recurse into properties values
|
||||||
|
if let Some(props) = obj.get_mut("properties") {
|
||||||
|
if let Some(props_obj) = props.as_object_mut() {
|
||||||
|
let keys: Vec<String> = props_obj.keys().cloned().collect();
|
||||||
|
for k in keys {
|
||||||
|
if let Some(v) = props_obj.get_mut(&k) {
|
||||||
|
normalize_object_schema(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recurse into items (arrays)
|
||||||
|
if let Some(items) = obj.get_mut("items") {
|
||||||
|
normalize_object_schema(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn openai_tool_definition(tool: &ToolDefinition) -> Value {
|
fn openai_tool_definition(tool: &ToolDefinition) -> Value {
|
||||||
|
let mut parameters = tool.input_schema.clone();
|
||||||
|
normalize_object_schema(&mut parameters);
|
||||||
json!({
|
json!({
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": tool.name,
|
"name": tool.name,
|
||||||
"description": tool.description,
|
"description": tool.description,
|
||||||
"parameters": tool.input_schema,
|
"parameters": parameters,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1122,6 +1188,76 @@ mod tests {
|
|||||||
assert_eq!(payload["tool_choice"], json!("auto"));
|
assert_eq!(payload["tool_choice"], json!("auto"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_schema_object_gets_strict_fields_for_responses_endpoint() {
|
||||||
|
// OpenAI /responses endpoint rejects object schemas missing
|
||||||
|
// "properties" and "additionalProperties". Verify normalize_object_schema
|
||||||
|
// fills them in so the request shape is strict-validator-safe.
|
||||||
|
use super::normalize_object_schema;
|
||||||
|
|
||||||
|
// Bare object — no properties at all
|
||||||
|
let mut schema = json!({"type": "object"});
|
||||||
|
normalize_object_schema(&mut schema);
|
||||||
|
assert_eq!(schema["properties"], json!({}));
|
||||||
|
assert_eq!(schema["additionalProperties"], json!(false));
|
||||||
|
|
||||||
|
// Nested object inside properties
|
||||||
|
let mut schema2 = json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {"type": "object", "properties": {"lat": {"type": "number"}}}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
normalize_object_schema(&mut schema2);
|
||||||
|
assert_eq!(schema2["additionalProperties"], json!(false));
|
||||||
|
assert_eq!(
|
||||||
|
schema2["properties"]["location"]["additionalProperties"],
|
||||||
|
json!(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Existing properties/additionalProperties should not be overwritten
|
||||||
|
let mut schema3 = json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"x": {"type": "string"}},
|
||||||
|
"additionalProperties": true
|
||||||
|
});
|
||||||
|
normalize_object_schema(&mut schema3);
|
||||||
|
assert_eq!(
|
||||||
|
schema3["additionalProperties"],
|
||||||
|
json!(true),
|
||||||
|
"must not overwrite existing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reasoning_effort_is_included_when_set() {
|
||||||
|
let payload = build_chat_completion_request(
|
||||||
|
&MessageRequest {
|
||||||
|
model: "o4-mini".to_string(),
|
||||||
|
max_tokens: 1024,
|
||||||
|
messages: vec![InputMessage::user_text("think hard")],
|
||||||
|
reasoning_effort: Some("high".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
OpenAiCompatConfig::openai(),
|
||||||
|
);
|
||||||
|
assert_eq!(payload["reasoning_effort"], json!("high"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reasoning_effort_omitted_when_not_set() {
|
||||||
|
let payload = build_chat_completion_request(
|
||||||
|
&MessageRequest {
|
||||||
|
model: "gpt-4o".to_string(),
|
||||||
|
max_tokens: 64,
|
||||||
|
messages: vec![InputMessage::user_text("hello")],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
OpenAiCompatConfig::openai(),
|
||||||
|
);
|
||||||
|
assert!(payload.get("reasoning_effort").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_streaming_requests_include_usage_opt_in() {
|
fn openai_streaming_requests_include_usage_opt_in() {
|
||||||
let payload = build_chat_completion_request(
|
let payload = build_chat_completion_request(
|
||||||
@@ -1239,6 +1375,7 @@ mod tests {
|
|||||||
frequency_penalty: Some(0.5),
|
frequency_penalty: Some(0.5),
|
||||||
presence_penalty: Some(0.3),
|
presence_penalty: Some(0.3),
|
||||||
stop: Some(vec!["\n".to_string()]),
|
stop: Some(vec!["\n".to_string()]),
|
||||||
|
reasoning_effort: None,
|
||||||
};
|
};
|
||||||
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
||||||
assert_eq!(payload["temperature"], 0.7);
|
assert_eq!(payload["temperature"], 0.7);
|
||||||
@@ -1323,4 +1460,45 @@ mod tests {
|
|||||||
assert!(payload.get("presence_penalty").is_none());
|
assert!(payload.get("presence_penalty").is_none());
|
||||||
assert!(payload.get("stop").is_none());
|
assert!(payload.get("stop").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gpt5_uses_max_completion_tokens_not_max_tokens() {
|
||||||
|
// gpt-5* models require `max_completion_tokens`; legacy `max_tokens` causes
|
||||||
|
// a request-validation failure. Verify the correct key is emitted.
|
||||||
|
let request = MessageRequest {
|
||||||
|
model: "gpt-5.2".to_string(),
|
||||||
|
max_tokens: 512,
|
||||||
|
messages: vec![],
|
||||||
|
stream: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
||||||
|
assert_eq!(
|
||||||
|
payload["max_completion_tokens"],
|
||||||
|
json!(512),
|
||||||
|
"gpt-5.2 should emit max_completion_tokens"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
payload.get("max_tokens").is_none(),
|
||||||
|
"gpt-5.2 must not emit max_tokens"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_gpt5_uses_max_tokens() {
|
||||||
|
// Older OpenAI models expect `max_tokens`; verify gpt-4o is unaffected.
|
||||||
|
let request = MessageRequest {
|
||||||
|
model: "gpt-4o".to_string(),
|
||||||
|
max_tokens: 512,
|
||||||
|
messages: vec![],
|
||||||
|
stream: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
||||||
|
assert_eq!(payload["max_tokens"], json!(512));
|
||||||
|
assert!(
|
||||||
|
payload.get("max_completion_tokens").is_none(),
|
||||||
|
"gpt-4o must not emit max_completion_tokens"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ pub struct MessageRequest {
|
|||||||
pub presence_penalty: Option<f64>,
|
pub presence_penalty: Option<f64>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub stop: Option<Vec<String>>,
|
pub stop: Option<Vec<String>>,
|
||||||
|
/// Reasoning effort level for OpenAI-compatible reasoning models (e.g. `o4-mini`).
|
||||||
|
/// Accepted values: `"low"`, `"medium"`, `"high"`. Omitted when `None`.
|
||||||
|
/// Silently ignored by backends that do not support it.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reasoning_effort: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageRequest {
|
impl MessageRequest {
|
||||||
|
|||||||
@@ -1938,6 +1938,42 @@ pub fn suggest_slash_commands(input: &str, limit: usize) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
/// Render the slash-command help section, optionally excluding stub commands
|
||||||
|
/// (commands that are registered in the spec list but not yet implemented).
|
||||||
|
/// Pass an empty slice to include all commands.
|
||||||
|
pub fn render_slash_command_help_filtered(exclude: &[&str]) -> String {
|
||||||
|
let mut lines = vec![
|
||||||
|
"Slash commands".to_string(),
|
||||||
|
" Start here /status, /diff, /agents, /skills, /commit".to_string(),
|
||||||
|
" [resume] also works with --resume SESSION.jsonl".to_string(),
|
||||||
|
String::new(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let categories = ["Session", "Tools", "Config", "Debug"];
|
||||||
|
|
||||||
|
for category in categories {
|
||||||
|
lines.push(category.to_string());
|
||||||
|
for spec in slash_command_specs()
|
||||||
|
.iter()
|
||||||
|
.filter(|spec| slash_command_category(spec.name) == category)
|
||||||
|
.filter(|spec| !exclude.contains(&spec.name))
|
||||||
|
{
|
||||||
|
lines.push(format_slash_command_help_line(spec));
|
||||||
|
}
|
||||||
|
lines.push(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
lines
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.skip_while(String::is_empty)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_slash_command_help() -> String {
|
pub fn render_slash_command_help() -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
"Slash commands".to_string(),
|
"Slash commands".to_string(),
|
||||||
|
|||||||
@@ -561,43 +561,4 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn output_with_stdin_tolerates_broken_pipe_when_child_closes_stdin_early() {
|
|
||||||
// given: a hook that immediately closes stdin without consuming the
|
|
||||||
// JSON payload. Use an oversized payload so the parent keeps writing
|
|
||||||
// long enough for Linux to surface EPIPE on the old implementation.
|
|
||||||
let root = temp_dir("stdin-close");
|
|
||||||
let script = root.join("close-stdin.sh");
|
|
||||||
fs::create_dir_all(&root).expect("temp hook dir");
|
|
||||||
fs::write(
|
|
||||||
&script,
|
|
||||||
"#!/bin/sh\nexec 0<&-\nprintf 'stdin closed early\\n'\nsleep 0.05\n",
|
|
||||||
)
|
|
||||||
.expect("write stdin-closing hook");
|
|
||||||
make_executable(&script);
|
|
||||||
|
|
||||||
let mut child = super::shell_command(script.to_str().expect("utf8 path"));
|
|
||||||
child.stdin(std::process::Stdio::piped());
|
|
||||||
child.stdout(std::process::Stdio::piped());
|
|
||||||
child.stderr(std::process::Stdio::piped());
|
|
||||||
let large_input = vec![b'x'; 2 * 1024 * 1024];
|
|
||||||
|
|
||||||
// when
|
|
||||||
let output = child
|
|
||||||
.output_with_stdin(&large_input)
|
|
||||||
.expect("broken pipe should be tolerated");
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert!(
|
|
||||||
output.status.success(),
|
|
||||||
"child should still exit cleanly: {output:?}"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
String::from_utf8_lossy(&output.stdout).trim(),
|
|
||||||
"stdin closed early"
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(root);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,6 +504,10 @@ where
|
|||||||
&self.session
|
&self.session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn api_client_mut(&mut self) -> &mut C {
|
||||||
|
&mut self.api_client
|
||||||
|
}
|
||||||
|
|
||||||
pub fn session_mut(&mut self) -> &mut Session {
|
pub fn session_mut(&mut self) -> &mut Session {
|
||||||
&mut self.session
|
&mut self.session
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ use commands::{
|
|||||||
classify_skills_slash_command, handle_agents_slash_command, handle_agents_slash_command_json,
|
classify_skills_slash_command, handle_agents_slash_command, handle_agents_slash_command_json,
|
||||||
handle_mcp_slash_command, handle_mcp_slash_command_json, handle_plugins_slash_command,
|
handle_mcp_slash_command, handle_mcp_slash_command_json, handle_plugins_slash_command,
|
||||||
handle_skills_slash_command, handle_skills_slash_command_json, render_slash_command_help,
|
handle_skills_slash_command, handle_skills_slash_command_json, render_slash_command_help,
|
||||||
resume_supported_slash_commands, slash_command_specs, validate_slash_command_input,
|
render_slash_command_help_filtered, resolve_skill_invocation, resume_supported_slash_commands,
|
||||||
SkillSlashDispatch, SlashCommand,
|
slash_command_specs, validate_slash_command_input, SkillSlashDispatch, SlashCommand,
|
||||||
};
|
};
|
||||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||||
use init::initialize_repo;
|
use init::initialize_repo;
|
||||||
@@ -110,7 +110,22 @@ type RuntimePluginStateBuildOutput = (
|
|||||||
fn main() {
|
fn main() {
|
||||||
if let Err(error) = run() {
|
if let Err(error) = run() {
|
||||||
let message = error.to_string();
|
let message = error.to_string();
|
||||||
if message.contains("`claw --help`") {
|
// When --output-format json is active, emit errors as JSON so downstream
|
||||||
|
// tools can parse failures the same way they parse successes (ROADMAP #42).
|
||||||
|
let argv: Vec<String> = std::env::args().collect();
|
||||||
|
let json_output = argv
|
||||||
|
.windows(2)
|
||||||
|
.any(|w| w[0] == "--output-format" && w[1] == "json")
|
||||||
|
|| argv.iter().any(|a| a == "--output-format=json");
|
||||||
|
if json_output {
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "error",
|
||||||
|
"error": message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if message.contains("`claw --help`") {
|
||||||
eprintln!("error: {message}");
|
eprintln!("error: {message}");
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -209,6 +224,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
|
reasoning_effort,
|
||||||
} => {
|
} => {
|
||||||
run_stale_base_preflight(base_commit.as_deref());
|
run_stale_base_preflight(base_commit.as_deref());
|
||||||
// Only consume piped stdin as prompt context when the permission
|
// Only consume piped stdin as prompt context when the permission
|
||||||
@@ -222,11 +238,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
|
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
|
||||||
LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output(
|
let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
|
||||||
&effective_prompt,
|
cli.set_reasoning_effort(reasoning_effort);
|
||||||
output_format,
|
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
|
||||||
compact,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
CliAction::Login { output_format } => run_login(output_format)?,
|
CliAction::Login { output_format } => run_login(output_format)?,
|
||||||
CliAction::Logout { output_format } => run_logout(output_format)?,
|
CliAction::Logout { output_format } => run_logout(output_format)?,
|
||||||
@@ -243,7 +257,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
base_commit,
|
base_commit,
|
||||||
} => run_repl(model, allowed_tools, permission_mode, base_commit)?,
|
reasoning_effort,
|
||||||
|
} => run_repl(
|
||||||
|
model,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
base_commit,
|
||||||
|
reasoning_effort,
|
||||||
|
)?,
|
||||||
CliAction::HelpTopic(topic) => print_help_topic(topic),
|
CliAction::HelpTopic(topic) => print_help_topic(topic),
|
||||||
CliAction::Help { output_format } => print_help(output_format)?,
|
CliAction::Help { output_format } => print_help(output_format)?,
|
||||||
}
|
}
|
||||||
@@ -304,6 +325,7 @@ enum CliAction {
|
|||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
compact: bool,
|
compact: bool,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
|
reasoning_effort: Option<String>,
|
||||||
},
|
},
|
||||||
Login {
|
Login {
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
@@ -330,6 +352,7 @@ enum CliAction {
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
|
reasoning_effort: Option<String>,
|
||||||
},
|
},
|
||||||
HelpTopic(LocalHelpTopic),
|
HelpTopic(LocalHelpTopic),
|
||||||
// prompt-mode formatting is only supported for non-interactive runs
|
// prompt-mode formatting is only supported for non-interactive runs
|
||||||
@@ -373,7 +396,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
let mut allowed_tool_values = Vec::new();
|
let mut allowed_tool_values = Vec::new();
|
||||||
let mut compact = false;
|
let mut compact = false;
|
||||||
let mut base_commit: Option<String> = None;
|
let mut base_commit: Option<String> = None;
|
||||||
let mut rest = Vec::new();
|
let mut reasoning_effort: Option<String> = None;
|
||||||
|
let mut rest: Vec<String> = Vec::new();
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
|
||||||
while index < args.len() {
|
while index < args.len() {
|
||||||
@@ -382,6 +406,31 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
wants_help = true;
|
wants_help = true;
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
"--help" | "-h"
|
||||||
|
if !rest.is_empty()
|
||||||
|
&& matches!(
|
||||||
|
rest[0].as_str(),
|
||||||
|
"prompt"
|
||||||
|
| "login"
|
||||||
|
| "logout"
|
||||||
|
| "version"
|
||||||
|
| "state"
|
||||||
|
| "init"
|
||||||
|
| "export"
|
||||||
|
| "commit"
|
||||||
|
| "pr"
|
||||||
|
| "issue"
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
// `--help` following a subcommand that would otherwise forward
|
||||||
|
// the arg to the API (e.g. `claw prompt --help`) should show
|
||||||
|
// top-level help instead. Subcommands that consume their own
|
||||||
|
// args (agents, mcp, plugins, skills) and local help-topic
|
||||||
|
// subcommands (status, sandbox, doctor) must NOT be intercepted
|
||||||
|
// here — they handle --help in their own dispatch paths.
|
||||||
|
wants_help = true;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
"--version" | "-V" => {
|
"--version" | "-V" => {
|
||||||
wants_version = true;
|
wants_version = true;
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -438,6 +487,28 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
base_commit = Some(flag[14..].to_string());
|
base_commit = Some(flag[14..].to_string());
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
"--reasoning-effort" => {
|
||||||
|
let value = args
|
||||||
|
.get(index + 1)
|
||||||
|
.ok_or_else(|| "missing value for --reasoning-effort".to_string())?;
|
||||||
|
if !matches!(value.as_str(), "low" | "medium" | "high") {
|
||||||
|
return Err(format!(
|
||||||
|
"invalid value for --reasoning-effort: '{value}'; must be low, medium, or high"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
reasoning_effort = Some(value.clone());
|
||||||
|
index += 2;
|
||||||
|
}
|
||||||
|
flag if flag.starts_with("--reasoning-effort=") => {
|
||||||
|
let value = &flag[19..];
|
||||||
|
if !matches!(value, "low" | "medium" | "high") {
|
||||||
|
return Err(format!(
|
||||||
|
"invalid value for --reasoning-effort: '{value}'; must be low, medium, or high"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
reasoning_effort = Some(value.to_string());
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
"-p" => {
|
"-p" => {
|
||||||
// Claw Code compat: -p "prompt" = one-shot prompt
|
// Claw Code compat: -p "prompt" = one-shot prompt
|
||||||
let prompt = args[index + 1..].join(" ");
|
let prompt = args[index + 1..].join(" ");
|
||||||
@@ -453,6 +524,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
.unwrap_or_else(default_permission_mode),
|
.unwrap_or_else(default_permission_mode),
|
||||||
compact,
|
compact,
|
||||||
base_commit: base_commit.clone(),
|
base_commit: base_commit.clone(),
|
||||||
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"--print" => {
|
"--print" => {
|
||||||
@@ -511,6 +583,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
base_commit,
|
base_commit,
|
||||||
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if rest.first().map(String::as_str) == Some("--resume") {
|
if rest.first().map(String::as_str) == Some("--resume") {
|
||||||
@@ -549,6 +622,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
}),
|
}),
|
||||||
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||||
args,
|
args,
|
||||||
@@ -574,6 +648,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
compact,
|
compact,
|
||||||
base_commit: base_commit.clone(),
|
base_commit: base_commit.clone(),
|
||||||
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
other if other.starts_with('/') => parse_direct_slash_cli_action(
|
other if other.starts_with('/') => parse_direct_slash_cli_action(
|
||||||
@@ -584,6 +659,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
|
reasoning_effort,
|
||||||
),
|
),
|
||||||
_other => Ok(CliAction::Prompt {
|
_other => Ok(CliAction::Prompt {
|
||||||
prompt: rest.join(" "),
|
prompt: rest.join(" "),
|
||||||
@@ -593,6 +669,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -686,6 +763,7 @@ fn parse_direct_slash_cli_action(
|
|||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
compact: bool,
|
compact: bool,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
|
reasoning_effort: Option<String>,
|
||||||
) -> Result<CliAction, String> {
|
) -> Result<CliAction, String> {
|
||||||
let raw = rest.join(" ");
|
let raw = rest.join(" ");
|
||||||
match SlashCommand::parse(&raw) {
|
match SlashCommand::parse(&raw) {
|
||||||
@@ -713,6 +791,7 @@ fn parse_direct_slash_cli_action(
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
}),
|
}),
|
||||||
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||||
args,
|
args,
|
||||||
@@ -1348,16 +1427,16 @@ fn run_worker_state(output_format: CliOutputFormat) -> Result<(), Box<dyn std::e
|
|||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
let state_path = cwd.join(".claw").join("worker-state.json");
|
let state_path = cwd.join(".claw").join("worker-state.json");
|
||||||
if !state_path.exists() {
|
if !state_path.exists() {
|
||||||
match output_format {
|
// Emit a structured error, then return Err so the process exits 1.
|
||||||
CliOutputFormat::Text => {
|
// Callers (scripts, CI) need a non-zero exit to detect "no state" without
|
||||||
println!("No worker state file found at {}", state_path.display())
|
// parsing prose output.
|
||||||
}
|
// Let the error propagate to main() which will format it correctly
|
||||||
CliOutputFormat::Json => println!(
|
// (prose for text mode, JSON envelope for --output-format json).
|
||||||
"{}",
|
return Err(format!(
|
||||||
serde_json::json!({"error": "no_state_file", "path": state_path.display().to_string()})
|
"no worker state file found at {} — run a worker first",
|
||||||
),
|
state_path.display()
|
||||||
}
|
)
|
||||||
return Ok(());
|
.into());
|
||||||
}
|
}
|
||||||
let raw = std::fs::read_to_string(&state_path)?;
|
let raw = std::fs::read_to_string(&state_path)?;
|
||||||
match output_format {
|
match output_format {
|
||||||
@@ -2762,10 +2841,12 @@ fn run_repl(
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
|
reasoning_effort: Option<String>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
run_stale_base_preflight(base_commit.as_deref());
|
run_stale_base_preflight(base_commit.as_deref());
|
||||||
let resolved_model = resolve_repl_model(model);
|
let resolved_model = resolve_repl_model(model);
|
||||||
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
||||||
|
cli.set_reasoning_effort(reasoning_effort);
|
||||||
let mut editor =
|
let mut editor =
|
||||||
input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default());
|
input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default());
|
||||||
println!("{}", cli.startup_banner());
|
println!("{}", cli.startup_banner());
|
||||||
@@ -2796,6 +2877,26 @@ fn run_repl(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Bare-word skill dispatch: if the first token of the input
|
||||||
|
// matches a known skill name, invoke it as `/skills <input>`
|
||||||
|
// rather than forwarding raw text to the LLM (ROADMAP #36).
|
||||||
|
let bare_first_token = trimmed.split_whitespace().next().unwrap_or_default();
|
||||||
|
let looks_like_skill_name = !bare_first_token.is_empty()
|
||||||
|
&& !bare_first_token.starts_with('/')
|
||||||
|
&& bare_first_token
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_alphanumeric() || c == '-' || c == '_');
|
||||||
|
if looks_like_skill_name {
|
||||||
|
let cwd = std::env::current_dir().unwrap_or_default();
|
||||||
|
if let Ok(SkillSlashDispatch::Invoke(prompt)) =
|
||||||
|
resolve_skill_invocation(&cwd, Some(&trimmed))
|
||||||
|
{
|
||||||
|
editor.push_history(input);
|
||||||
|
cli.record_prompt_history(&trimmed);
|
||||||
|
cli.run_turn(&prompt)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
editor.push_history(input);
|
editor.push_history(input);
|
||||||
cli.record_prompt_history(&trimmed);
|
cli.record_prompt_history(&trimmed);
|
||||||
cli.run_turn(&trimmed)?;
|
cli.run_turn(&trimmed)?;
|
||||||
@@ -3348,6 +3449,12 @@ impl LiveCli {
|
|||||||
Ok(cli)
|
Ok(cli)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_reasoning_effort(&mut self, effort: Option<String>) {
|
||||||
|
if let Some(rt) = self.runtime.runtime.as_mut() {
|
||||||
|
rt.api_client_mut().set_reasoning_effort(effort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn startup_banner(&self) -> String {
|
fn startup_banner(&self) -> String {
|
||||||
let cwd = env::current_dir().map_or_else(
|
let cwd = env::current_dir().map_or_else(
|
||||||
|_| "<unknown>".to_string(),
|
|_| "<unknown>".to_string(),
|
||||||
@@ -4650,7 +4757,7 @@ fn render_repl_help() -> String {
|
|||||||
" Browse sessions /session list".to_string(),
|
" Browse sessions /session list".to_string(),
|
||||||
" Show prompt history /history [count]".to_string(),
|
" Show prompt history /history [count]".to_string(),
|
||||||
String::new(),
|
String::new(),
|
||||||
render_slash_command_help(),
|
render_slash_command_help_filtered(STUB_COMMANDS),
|
||||||
]
|
]
|
||||||
.join(
|
.join(
|
||||||
"
|
"
|
||||||
@@ -6370,6 +6477,7 @@ struct AnthropicRuntimeClient {
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
tool_registry: GlobalToolRegistry,
|
tool_registry: GlobalToolRegistry,
|
||||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||||
|
reasoning_effort: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnthropicRuntimeClient {
|
impl AnthropicRuntimeClient {
|
||||||
@@ -6434,8 +6542,13 @@ impl AnthropicRuntimeClient {
|
|||||||
allowed_tools,
|
allowed_tools,
|
||||||
tool_registry,
|
tool_registry,
|
||||||
progress_reporter,
|
progress_reporter,
|
||||||
|
reasoning_effort: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_reasoning_effort(&mut self, effort: Option<String>) {
|
||||||
|
self.reasoning_effort = effort;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
|
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
|
||||||
@@ -6481,6 +6594,7 @@ impl ApiClient for AnthropicRuntimeClient {
|
|||||||
.then(|| filter_tool_specs(&self.tool_registry, self.allowed_tools.as_ref())),
|
.then(|| filter_tool_specs(&self.tool_registry, self.allowed_tools.as_ref())),
|
||||||
tool_choice: self.enable_tools.then_some(ToolChoice::Auto),
|
tool_choice: self.enable_tools.then_some(ToolChoice::Auto),
|
||||||
stream: true,
|
stream: true,
|
||||||
|
reasoning_effort: self.reasoning_effort.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -6857,6 +6971,51 @@ fn collect_prompt_cache_events(summary: &runtime::TurnSummary) -> Vec<serde_json
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Slash commands that are registered in the spec list but not yet implemented
|
||||||
|
/// in this build. Used to filter both REPL completions and help output so the
|
||||||
|
/// discovery surface only shows commands that actually work (ROADMAP #39).
|
||||||
|
const STUB_COMMANDS: &[&str] = &[
|
||||||
|
"login",
|
||||||
|
"logout",
|
||||||
|
"vim",
|
||||||
|
"upgrade",
|
||||||
|
"stats",
|
||||||
|
"share",
|
||||||
|
"feedback",
|
||||||
|
"files",
|
||||||
|
"fast",
|
||||||
|
"exit",
|
||||||
|
"summary",
|
||||||
|
"desktop",
|
||||||
|
"brief",
|
||||||
|
"advisor",
|
||||||
|
"stickers",
|
||||||
|
"insights",
|
||||||
|
"thinkback",
|
||||||
|
"release-notes",
|
||||||
|
"security-review",
|
||||||
|
"keybindings",
|
||||||
|
"privacy-settings",
|
||||||
|
"plan",
|
||||||
|
"review",
|
||||||
|
"tasks",
|
||||||
|
"theme",
|
||||||
|
"voice",
|
||||||
|
"usage",
|
||||||
|
"rename",
|
||||||
|
"copy",
|
||||||
|
"hooks",
|
||||||
|
"context",
|
||||||
|
"color",
|
||||||
|
"effort",
|
||||||
|
"branch",
|
||||||
|
"rewind",
|
||||||
|
"ide",
|
||||||
|
"tag",
|
||||||
|
"output-style",
|
||||||
|
"add-dir",
|
||||||
|
];
|
||||||
|
|
||||||
fn slash_command_completion_candidates_with_sessions(
|
fn slash_command_completion_candidates_with_sessions(
|
||||||
model: &str,
|
model: &str,
|
||||||
active_session_id: Option<&str>,
|
active_session_id: Option<&str>,
|
||||||
@@ -6865,9 +7024,14 @@ fn slash_command_completion_candidates_with_sessions(
|
|||||||
let mut completions = BTreeSet::new();
|
let mut completions = BTreeSet::new();
|
||||||
|
|
||||||
for spec in slash_command_specs() {
|
for spec in slash_command_specs() {
|
||||||
|
if STUB_COMMANDS.contains(&spec.name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
completions.insert(format!("/{}", spec.name));
|
completions.insert(format!("/{}", spec.name));
|
||||||
for alias in spec.aliases {
|
for alias in spec.aliases {
|
||||||
completions.insert(format!("/{alias}"));
|
if !STUB_COMMANDS.contains(alias) {
|
||||||
|
completions.insert(format!("/{alias}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7756,7 +7920,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
writeln!(out)?;
|
writeln!(out)?;
|
||||||
writeln!(out, "Interactive slash commands:")?;
|
writeln!(out, "Interactive slash commands:")?;
|
||||||
writeln!(out, "{}", render_slash_command_help())?;
|
writeln!(out, "{}", render_slash_command_help_filtered(STUB_COMMANDS))?;
|
||||||
writeln!(out)?;
|
writeln!(out)?;
|
||||||
let resume_commands = resume_supported_slash_commands()
|
let resume_commands = resume_supported_slash_commands()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -7850,7 +8014,7 @@ mod tests {
|
|||||||
summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture, CliAction,
|
summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture, CliAction,
|
||||||
CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent,
|
CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent,
|
||||||
InternalPromptProgressState, LiveCli, LocalHelpTopic, PromptHistoryEntry, SlashCommand,
|
InternalPromptProgressState, LiveCli, LocalHelpTopic, PromptHistoryEntry, SlashCommand,
|
||||||
StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE, STUB_COMMANDS,
|
||||||
};
|
};
|
||||||
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
||||||
use plugins::{
|
use plugins::{
|
||||||
@@ -8174,6 +8338,7 @@ mod tests {
|
|||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8337,6 +8502,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8426,6 +8592,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8455,6 +8622,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
compact: true,
|
compact: true,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8496,6 +8664,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8573,6 +8742,7 @@ mod tests {
|
|||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::ReadOnly,
|
permission_mode: PermissionMode::ReadOnly,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8592,6 +8762,7 @@ mod tests {
|
|||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8620,6 +8791,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8645,6 +8817,7 @@ mod tests {
|
|||||||
),
|
),
|
||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8754,6 +8927,7 @@ mod tests {
|
|||||||
permission_mode: crate::default_permission_mode(),
|
permission_mode: crate::default_permission_mode(),
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -9137,6 +9311,7 @@ mod tests {
|
|||||||
permission_mode: crate::default_permission_mode(),
|
permission_mode: crate::default_permission_mode(),
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9203,6 +9378,7 @@ mod tests {
|
|||||||
permission_mode: crate::default_permission_mode(),
|
permission_mode: crate::default_permission_mode(),
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -9228,6 +9404,7 @@ mod tests {
|
|||||||
permission_mode: crate::default_permission_mode(),
|
permission_mode: crate::default_permission_mode(),
|
||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let error = parse_args(&["/status".to_string()])
|
let error = parse_args(&["/status".to_string()])
|
||||||
@@ -10983,6 +11160,58 @@ UU conflicted.rs",
|
|||||||
let _ = fs::remove_dir_all(source_root);
|
let _ = fs::remove_dir_all(source_root);
|
||||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_reasoning_effort_value() {
|
||||||
|
let err = parse_args(&[
|
||||||
|
"--reasoning-effort".to_string(),
|
||||||
|
"turbo".to_string(),
|
||||||
|
"prompt".to_string(),
|
||||||
|
"hello".to_string(),
|
||||||
|
])
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
err.contains("invalid value for --reasoning-effort"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
|
assert!(err.contains("turbo"), "unexpected error: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_valid_reasoning_effort_values() {
|
||||||
|
for value in ["low", "medium", "high"] {
|
||||||
|
let result = parse_args(&[
|
||||||
|
"--reasoning-effort".to_string(),
|
||||||
|
value.to_string(),
|
||||||
|
"prompt".to_string(),
|
||||||
|
"hello".to_string(),
|
||||||
|
]);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"--reasoning-effort {value} should be accepted, got: {:?}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
if let Ok(CliAction::Prompt {
|
||||||
|
reasoning_effort, ..
|
||||||
|
}) = result
|
||||||
|
{
|
||||||
|
assert_eq!(reasoning_effort.as_deref(), Some(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stub_commands_absent_from_repl_completions() {
|
||||||
|
let candidates =
|
||||||
|
slash_command_completion_candidates_with_sessions("claude-3-5-sonnet", None, vec![]);
|
||||||
|
for stub in STUB_COMMANDS {
|
||||||
|
let with_slash = format!("/{stub}");
|
||||||
|
assert!(
|
||||||
|
!candidates.contains(&with_slash),
|
||||||
|
"stub command {with_slash} should not appear in REPL completions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_mcp_server_fixture(script_path: &Path) {
|
fn write_mcp_server_fixture(script_path: &Path) {
|
||||||
|
|||||||
Reference in New Issue
Block a user