Compare commits

...

15 Commits

Author SHA1 Message Date
YeonGyu-Kim
54269da157 fix(cli): claw state exits 1 when no worker state file exists
Previously 'claw state' printed an error message but exited 0, making it
impossible for scripts/CI to detect the absence of state without parsing
prose. Now propagates Err() to main() which exits 1 and formats the error
correctly for both text and --output-format json modes.

Text: 'error: no worker state file found at ... — run a worker first'
JSON: {"type":"error","error":"no worker state file found at ..."}
2026-04-09 18:34:41 +09:00
YeonGyu-Kim
f741a42507 test(cli): add regression coverage for reasoning-effort validation and stub-command filtering
3 new tests in mod tests:
- rejects_invalid_reasoning_effort_value: confirms 'turbo' etc rejected at parse time
- accepts_valid_reasoning_effort_values: confirms low/medium/high accepted and threaded
- stub_commands_absent_from_repl_completions: asserts STUB_COMMANDS are not in completions

156 -> 159 CLI tests pass.
2026-04-09 18:06:32 +09:00
YeonGyu-Kim
6b3e2d8854 docs(roadmap): file hook ingress opacity as ROADMAP #43 2026-04-09 17:34:15 +09:00
YeonGyu-Kim
1a8f73da01 fix(cli): emit JSON error on --output-format json — ROADMAP #42
When claw --output-format json hits an error, the error was previously
printed as plain prose to stderr, making it invisible to downstream tooling
that parses JSON output. Now:

  {"type":"error","error":"api returned 401 ..."}

Detection: scan argv at process exit for --output-format json or
--output-format=json. Non-JSON error path unchanged. 156 CLI tests pass.
2026-04-09 16:33:20 +09:00
YeonGyu-Kim
7d9f11b91f docs(roadmap): track community-support plugin-test-sealing as #41 2026-04-09 16:18:48 +09:00
YeonGyu-Kim
8e1bca6b99 docs(roadmap): track community-support plugin-list-load-failures as #40 2026-04-09 16:17:28 +09:00
YeonGyu-Kim
8d0308eecb fix(cli): dispatch bare skill names to skill invoker in REPL — ROADMAP #36
Users were typing skill names (e.g. 'caveman', 'find-skills') directly in
the REPL and getting LLM responses instead of skill invocation. Only
'/skills <name>' triggered dispatch; bare names fell through to run_turn.

Fix: after slash-command parse returns None (bare text), check if the first
token looks like a skill name (alphanumeric/dash/underscore, no slash).
If resolve_skill_invocation() confirms the skill exists, dispatch the full
input as a skill prompt. Unknown words fall through unchanged.

156 CLI tests pass, fmt clean.
2026-04-09 16:01:18 +09:00
YeonGyu-Kim
4d10caebc6 fix(cli): validate --reasoning-effort accepts only low|medium|high
Previously any string was accepted and silently forwarded to the API,
which would fail at the provider with an unhelpful error. Now invalid
values produce a clear error at parse time:

  invalid value for --reasoning-effort: 'xyz'; must be low, medium, or high

156 CLI tests pass, fmt clean.
2026-04-09 15:03:36 +09:00
YeonGyu-Kim
414526c1bd fix(cli): exclude stub slash commands from help output — ROADMAP #39
The --help slash-command section was listing ~35 unimplemented commands
alongside working ones. Combined with the completions fix (c55c510), the
discovery surface now consistently shows only implemented commands.

Changes:
- commands crate: add render_slash_command_help_filtered(exclude: &[&str])
- move STUB_COMMANDS to module-level const in main.rs (reused by both
  completions and help rendering)
- replace render_slash_command_help() with filtered variant at all
  help-rendering call sites

156 CLI tests pass, fmt clean.
2026-04-09 14:36:00 +09:00
YeonGyu-Kim
2a2e205414 fix(cli): intercept --help for prompt/login/logout/version subcommands before API dispatch
'claw prompt --help' was triggering an API call instead of showing help
because --help was parsed as part of the prompt args. Now '--help' after
known pass-through subcommands (prompt, login, logout, version, state,
init, export, commit, pr, issue) sets wants_help=true and shows the
top-level help page.

Subcommands that consume their own args (agents, mcp, plugins, skills)
and local help-topic subcommands (status, sandbox, doctor) are excluded
from this interception so their existing --help handling is preserved.

156 CLI tests pass, fmt clean.
2026-04-09 14:06:26 +09:00
YeonGyu-Kim
c55c510883 fix(cli): exclude stub slash commands from REPL completions — ROADMAP #39
Commands registered in the spec list but not yet implemented in this build
were appearing in REPL tab-completions, making the discovery surface
over-promise what actually works. Users (mezz2301) reported 'many features
are not supported' after discovering these through completions.

Add STUB_COMMANDS exclusion list in slash_command_completion_candidates_with_sessions.
Excluded: 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

These commands still parse and run (with the 'not yet implemented' message
for users who type them directly), but they no longer surface as
tab-completion candidates.
2026-04-09 13:36:12 +09:00
YeonGyu-Kim
3fe0caf348 docs(roadmap): file stub slash commands as ROADMAP #39 (/branch /rewind /ide /tag /output-style /add-dir) 2026-04-09 12:31:17 +09:00
YeonGyu-Kim
47086c1c14 docs(readme): fix cold-start quick-start sequence — set API key before prompt, add claw doctor step
The previous quick start jumped from 'cargo build' to 'claw prompt' without
showing the required auth step or the health-check command. A user following
it linearly would fail because the prompt needs an API key.

Changes:
- Numbered steps: build -> set ANTHROPIC_API_KEY -> claw doctor -> prompt
- Windows note updated to show cargo run form as alternative
- Added explicit NOTE that Claude subscription login is not supported (pre-empts #claw-code FAQ)

Source: cold-start friction observed from mezz/mukduk and kapcomunica in #claw-code 2026-04-09.
2026-04-09 12:00:59 +09:00
YeonGyu-Kim
e579902782 docs(readme): add Windows PowerShell note — binary is claw.exe not claw
User repro: mezz on Windows PowerShell tried './target/debug/claw'
which fails because the binary is 'claw.exe' on Windows.
Add a NOTE callout after the quick-start block directing Windows users
to use .\target\debug\claw.exe or cargo run -- --help.
2026-04-09 11:30:53 +09:00
YeonGyu-Kim
ca8950c26b feat(cli): wire --reasoning-effort flag end-to-end — closes ROADMAP #34
Parse --reasoning-effort <low|medium|high> in parse_args, thread through
CliAction::Prompt and CliAction::Repl, LiveCli::set_reasoning_effort(),
AnthropicRuntimeClient.reasoning_effort field, and MessageRequest.reasoning_effort.

Changes:
- parse_args: new --reasoning-effort / --reasoning-effort=VAL flag arms
- AnthropicRuntimeClient: new reasoning_effort field + set_reasoning_effort() method
- LiveCli: new set_reasoning_effort() that reaches through BuiltRuntime -> ConversationRuntime -> api_client_mut()
- runtime::ConversationRuntime: new pub api_client_mut() accessor
- MessageRequest construction: reasoning_effort: self.reasoning_effort.clone()
- run_repl(): accepts and applies reasoning_effort parameter
- parse_direct_slash_cli_action(): propagates reasoning_effort

All 156 CLI tests pass, all api tests pass, cargo fmt clean.
2026-04-09 11:08:00 +09:00
5 changed files with 304 additions and 41 deletions

View File

@@ -49,22 +49,27 @@ The canonical implementation lives in [`rust/`](./rust), and the current source
> **`cargo install clawcode` will not work** — this package is not published on crates.io. Build from source as shown below.
```bash
# 1. Clone and build
git clone https://github.com/ultraworkers/claw-code
cd claw-code/rust
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:
```bash
# 2. Set your API key (Anthropic API key — not a Claude subscription)
export ANTHROPIC_API_KEY="sk-ant-..."
# or
cd rust
./target/debug/claw login
# 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.
> [!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:
```bash

View File

@@ -500,3 +500,15 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
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.

View File

@@ -1938,6 +1938,42 @@ pub fn suggest_slash_commands(input: &str, limit: usize) -> Vec<String> {
}
#[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 {
let mut lines = vec![
"Slash commands".to_string(),

View File

@@ -504,6 +504,10 @@ where
&self.session
}
pub fn api_client_mut(&mut self) -> &mut C {
&mut self.api_client
}
pub fn session_mut(&mut self) -> &mut Session {
&mut self.session
}

View File

@@ -35,8 +35,8 @@ use commands::{
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_skills_slash_command, handle_skills_slash_command_json, render_slash_command_help,
resume_supported_slash_commands, slash_command_specs, validate_slash_command_input,
SkillSlashDispatch, SlashCommand,
render_slash_command_help_filtered, resolve_skill_invocation, resume_supported_slash_commands,
slash_command_specs, validate_slash_command_input, SkillSlashDispatch, SlashCommand,
};
use compat_harness::{extract_manifest, UpstreamPaths};
use init::initialize_repo;
@@ -110,7 +110,22 @@ type RuntimePluginStateBuildOutput = (
fn main() {
if let Err(error) = run() {
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}");
} else {
eprintln!(
@@ -209,7 +224,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
permission_mode,
compact,
base_commit,
..
reasoning_effort,
} => {
run_stale_base_preflight(base_commit.as_deref());
// Only consume piped stdin as prompt context when the permission
@@ -223,11 +238,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
None
};
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output(
&effective_prompt,
output_format,
compact,
)?;
let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
cli.set_reasoning_effort(reasoning_effort);
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
}
CliAction::Login { output_format } => run_login(output_format)?,
CliAction::Logout { output_format } => run_logout(output_format)?,
@@ -244,8 +257,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
allowed_tools,
permission_mode,
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::Help { output_format } => print_help(output_format)?,
}
@@ -377,7 +396,8 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let mut allowed_tool_values = Vec::new();
let mut compact = false;
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;
while index < args.len() {
@@ -386,6 +406,31 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
wants_help = true;
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" => {
wants_version = true;
index += 1;
@@ -442,6 +487,28 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
base_commit = Some(flag[14..].to_string());
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" => {
// Claw Code compat: -p "prompt" = one-shot prompt
let prompt = args[index + 1..].join(" ");
@@ -457,7 +524,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
.unwrap_or_else(default_permission_mode),
compact,
base_commit: base_commit.clone(),
reasoning_effort: None,
reasoning_effort: reasoning_effort.clone(),
});
}
"--print" => {
@@ -516,7 +583,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
allowed_tools,
permission_mode,
base_commit,
reasoning_effort: None,
reasoning_effort: reasoning_effort.clone(),
});
}
if rest.first().map(String::as_str) == Some("--resume") {
@@ -555,7 +622,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
permission_mode,
compact,
base_commit,
reasoning_effort: None,
reasoning_effort: reasoning_effort.clone(),
}),
SkillSlashDispatch::Local => Ok(CliAction::Skills {
args,
@@ -581,7 +648,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
permission_mode,
compact,
base_commit: base_commit.clone(),
reasoning_effort: None,
reasoning_effort: reasoning_effort.clone(),
})
}
other if other.starts_with('/') => parse_direct_slash_cli_action(
@@ -592,6 +659,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
permission_mode,
compact,
base_commit,
reasoning_effort,
),
_other => Ok(CliAction::Prompt {
prompt: rest.join(" "),
@@ -601,7 +669,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
permission_mode,
compact,
base_commit,
reasoning_effort: None,
reasoning_effort: reasoning_effort.clone(),
}),
}
}
@@ -695,6 +763,7 @@ fn parse_direct_slash_cli_action(
permission_mode: PermissionMode,
compact: bool,
base_commit: Option<String>,
reasoning_effort: Option<String>,
) -> Result<CliAction, String> {
let raw = rest.join(" ");
match SlashCommand::parse(&raw) {
@@ -722,7 +791,7 @@ fn parse_direct_slash_cli_action(
permission_mode,
compact,
base_commit,
reasoning_effort: None,
reasoning_effort: reasoning_effort.clone(),
}),
SkillSlashDispatch::Local => Ok(CliAction::Skills {
args,
@@ -1358,16 +1427,16 @@ fn run_worker_state(output_format: CliOutputFormat) -> Result<(), Box<dyn std::e
let cwd = env::current_dir()?;
let state_path = cwd.join(".claw").join("worker-state.json");
if !state_path.exists() {
match output_format {
CliOutputFormat::Text => {
println!("No worker state file found at {}", state_path.display())
}
CliOutputFormat::Json => println!(
"{}",
serde_json::json!({"error": "no_state_file", "path": state_path.display().to_string()})
),
}
return Ok(());
// Emit a structured error, then return Err so the process exits 1.
// Callers (scripts, CI) need a non-zero exit to detect "no state" without
// parsing prose output.
// Let the error propagate to main() which will format it correctly
// (prose for text mode, JSON envelope for --output-format json).
return Err(format!(
"no worker state file found at {} — run a worker first",
state_path.display()
)
.into());
}
let raw = std::fs::read_to_string(&state_path)?;
match output_format {
@@ -2772,10 +2841,12 @@ fn run_repl(
allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode,
base_commit: Option<String>,
reasoning_effort: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
run_stale_base_preflight(base_commit.as_deref());
let resolved_model = resolve_repl_model(model);
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
cli.set_reasoning_effort(reasoning_effort);
let mut editor =
input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default());
println!("{}", cli.startup_banner());
@@ -2806,6 +2877,26 @@ fn run_repl(
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);
cli.record_prompt_history(&trimmed);
cli.run_turn(&trimmed)?;
@@ -3358,6 +3449,12 @@ impl LiveCli {
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 {
let cwd = env::current_dir().map_or_else(
|_| "<unknown>".to_string(),
@@ -4660,7 +4757,7 @@ fn render_repl_help() -> String {
" Browse sessions /session list".to_string(),
" Show prompt history /history [count]".to_string(),
String::new(),
render_slash_command_help(),
render_slash_command_help_filtered(STUB_COMMANDS),
]
.join(
"
@@ -6380,6 +6477,7 @@ struct AnthropicRuntimeClient {
allowed_tools: Option<AllowedToolSet>,
tool_registry: GlobalToolRegistry,
progress_reporter: Option<InternalPromptProgressReporter>,
reasoning_effort: Option<String>,
}
impl AnthropicRuntimeClient {
@@ -6444,8 +6542,13 @@ impl AnthropicRuntimeClient {
allowed_tools,
tool_registry,
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>> {
@@ -6491,6 +6594,7 @@ impl ApiClient for AnthropicRuntimeClient {
.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()
};
@@ -6867,6 +6971,51 @@ fn collect_prompt_cache_events(summary: &runtime::TurnSummary) -> Vec<serde_json
.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(
model: &str,
active_session_id: Option<&str>,
@@ -6875,9 +7024,14 @@ fn slash_command_completion_candidates_with_sessions(
let mut completions = BTreeSet::new();
for spec in slash_command_specs() {
if STUB_COMMANDS.contains(&spec.name) {
continue;
}
completions.insert(format!("/{}", spec.name));
for alias in spec.aliases {
completions.insert(format!("/{alias}"));
if !STUB_COMMANDS.contains(alias) {
completions.insert(format!("/{alias}"));
}
}
}
@@ -7766,7 +7920,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
)?;
writeln!(out)?;
writeln!(out, "Interactive slash commands:")?;
writeln!(out, "{}", render_slash_command_help())?;
writeln!(out, "{}", render_slash_command_help_filtered(STUB_COMMANDS))?;
writeln!(out)?;
let resume_commands = resume_supported_slash_commands()
.into_iter()
@@ -7860,7 +8014,7 @@ mod tests {
summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture, CliAction,
CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent,
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 plugins::{
@@ -11006,6 +11160,58 @@ UU conflicted.rs",
let _ = fs::remove_dir_all(source_root);
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) {