This commit is contained in:
thiagomargatho 2026-07-07 12:16:34 +00:00 committed by GitHub
commit a1eb5a4f49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 12561 additions and 12045 deletions

View File

@ -17,5 +17,5 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Working agreement
- Prefer small, reviewable changes and keep generated bootstrap files aligned with actual repo workflows.
- Keep shared defaults in `.claude.json`; reserve `.claude/settings.local.json` for machine-local overrides.
- Keep shared defaults in `.claw.json`; reserve `.claw/settings.local.json` for machine-local overrides.
- Do not overwrite existing `CLAUDE.md` content automatically; update it intentionally when repo workflows change.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,211 @@
use crate::*;
use api::{
detect_provider_kind, model_family_identity_for, resolve_startup_auth_source, AnthropicClient,
AuthSource, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest,
MessageResponse, OutputContentBlock, PromptCache, ProviderClient as ApiProviderClient,
ProviderKind, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
ToolResultContentBlock,
};
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,
render_slash_command_help_filtered, resolve_skill_invocation, resume_supported_slash_commands,
slash_command_specs, validate_slash_command_input, PluginsCommandResult, SkillSlashDispatch,
SlashCommand,
};
use init::initialize_repo;
use log::debug;
use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
use runtime::{
check_base_commit, format_stale_base_warning, format_usd, load_oauth_credentials,
load_system_prompt, load_system_prompt_with_context, pricing_for_model, resolve_expected_base,
resolve_sandbox_status, ApiClient, ApiRequest, AssistantEvent, BaseCommitState,
CompactionConfig, ConfigFileReport, ConfigLoader, ConfigSource, ContentBlock, ContextFile,
ConversationMessage, ConversationRuntime, McpConfigCollection, McpInvalidServerConfig,
McpServer, McpServerManager, McpServerSpec, McpTool, MessageRole, ModelPricing, PermissionMode,
PermissionPolicy, ProjectContext, PromptCacheEvent, ResolvedPermissionMode, RuntimeError,
RuntimeInvalidHookConfig, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
};
use serde::Deserialize;
use serde_json::{json, Map, Value};
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::io::{self, IsTerminal, Read, Write};
use std::net::TcpListener;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant, UNIX_EPOCH};
use tools::{
canonical_allowed_tool_name, execute_tool, mvp_tool_specs, GlobalToolRegistry,
RuntimeToolDefinition, ToolSearchOutput,
};
/// #148: Model provenance for `claw status` JSON/text output. Records where
/// the resolved model string came from so claws don't have to re-read argv
/// to audit whether their `--model` flag was honored vs falling back to env
/// or config or default.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ModelSource {
/// Explicit `--model` / `--model=` CLI flag.
Flag,
/// Runtime model environment variable (when no flag was passed).
Env,
/// `model` key in `.claw.json` / `.claw/settings.json` (when neither
/// flag nor env set it).
Config,
/// Compiled-in `DEFAULT_MODEL` fallback.
Default,
}
impl ModelSource {
pub(crate) fn as_str(&self) -> &'static str {
match self {
ModelSource::Flag => "flag",
ModelSource::Env => "env",
ModelSource::Config => "config",
ModelSource::Default => "default",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ModelProvenance {
/// Resolved model string (after alias expansion).
pub(crate) resolved: String,
/// Raw user input before alias resolution. None when source is Default.
pub(crate) raw: Option<String>,
/// Where the resolved model string originated.
pub(crate) source: ModelSource,
/// Alias-expanded target when `raw` differs from `resolved`.
pub(crate) alias_resolved_to: Option<String>,
/// Environment variable that supplied the model, when source is Env.
pub(crate) env_var: Option<String>,
}
impl ModelProvenance {
pub(crate) fn default_fallback() -> Self {
Self {
resolved: DEFAULT_MODEL.to_string(),
raw: None,
source: ModelSource::Default,
alias_resolved_to: None,
env_var: None,
}
}
pub(crate) fn from_flag(raw: &str, resolved: &str) -> Self {
Self::from_resolved(raw, resolved, ModelSource::Flag, None)
}
pub(crate) fn from_raw(raw: &str, source: ModelSource, env_var: Option<&str>) -> Self {
let resolved = resolve_model_alias_with_config(raw);
Self::from_resolved(raw, &resolved, source, env_var)
}
pub(crate) fn from_resolved(
raw: &str,
resolved: &str,
source: ModelSource,
env_var: Option<&str>,
) -> Self {
let raw_trimmed = raw.trim();
let alias_resolved_to = (raw_trimmed != resolved).then(|| resolved.to_string());
Self {
resolved: resolved.to_string(),
raw: Some(raw.to_string()),
source,
alias_resolved_to,
env_var: env_var.map(str::to_string),
}
}
pub(crate) fn from_env_or_config_or_default(cli_model: &str) -> Result<Self, String> {
// Only called when no --model flag was passed. Probe env first,
// then config, else fall back to default. Mirrors the logic in
// resolve_repl_model() but captures the source.
if cli_model != DEFAULT_MODEL {
let provenance = Self::from_resolved(cli_model, cli_model, ModelSource::Flag, None);
provenance.validate()?;
return Ok(provenance);
}
if let Some(env_model) = env_model_for_runtime() {
let provenance =
Self::from_raw(&env_model.value, ModelSource::Env, Some(env_model.name));
provenance.validate()?;
return Ok(provenance);
}
if let Some(config_model) = config_model_for_current_dir() {
let provenance = Self::from_raw(&config_model, ModelSource::Config, None);
provenance.validate()?;
return Ok(provenance);
}
Ok(Self::default_fallback())
}
pub(crate) fn validate(&self) -> Result<(), String> {
validate_model_syntax(&self.resolved).map_err(|error| {
let source = match self.source {
ModelSource::Flag => "--model",
ModelSource::Env => self.env_var.as_deref().unwrap_or("environment"),
ModelSource::Config => "config model",
ModelSource::Default => "default model",
};
if let Some(raw) = &self.raw {
format!(
"invalid_model: {source} model `{raw}` is invalid after alias resolution to `{}`.\n{error}",
self.resolved
)
} else {
error
}
})
}
}
pub(crate) fn resolve_model_alias(model: &str) -> &str {
match model {
"opus" => "anthropic/claude-opus-4-7",
"sonnet" => "anthropic/claude-sonnet-4-6",
"haiku" => "anthropic/claude-haiku-4-5-20251213",
_ => model,
}
}
/// Resolve a model name through user-defined config aliases first, then fall
/// back to the built-in alias table. This is the entry point used wherever a
/// user-supplied model string is about to be dispatched to a provider.
pub(crate) fn resolve_model_alias_with_config(model: &str) -> String {
let trimmed = model.trim();
if let Some(resolved) = config_alias_for_current_dir(trimmed) {
return resolve_model_alias(&resolved).to_string();
}
resolve_model_alias(trimmed).to_string()
}
pub(crate) fn config_alias_for_current_dir(alias: &str) -> Option<String> {
if alias.is_empty() {
return None;
}
let cwd = env::current_dir().ok()?;
let loader = ConfigLoader::default_for(&cwd);
let config = loader.load().ok()?;
config.aliases().get(alias).cloned()
}
pub(crate) fn config_model_for_current_dir() -> Option<String> {
let cwd = env::current_dir().ok()?;
let loader = ConfigLoader::default_for(&cwd);
loader.load().ok()?.model().map(ToOwned::to_owned)
}
pub(crate) fn resolve_repl_model(cli_model: String) -> Result<String, String> {
Ok(ModelProvenance::from_env_or_config_or_default(&cli_model)?.resolved)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,416 @@
use crate::*;
use api::{
detect_provider_kind, model_family_identity_for, resolve_startup_auth_source, AnthropicClient,
AuthSource, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest,
MessageResponse, OutputContentBlock, PromptCache, ProviderClient as ApiProviderClient,
ProviderKind, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
ToolResultContentBlock,
};
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,
render_slash_command_help_filtered, resolve_skill_invocation, resume_supported_slash_commands,
slash_command_specs, validate_slash_command_input, PluginsCommandResult, SkillSlashDispatch,
SlashCommand,
};
use init::initialize_repo;
use log::debug;
use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
use runtime::{
check_base_commit, format_stale_base_warning, format_usd, load_oauth_credentials,
load_system_prompt, load_system_prompt_with_context, pricing_for_model, resolve_expected_base,
resolve_sandbox_status, ApiClient, ApiRequest, AssistantEvent, BaseCommitState,
CompactionConfig, ConfigFileReport, ConfigLoader, ConfigSource, ContentBlock, ContextFile,
ConversationMessage, ConversationRuntime, McpConfigCollection, McpInvalidServerConfig,
McpServer, McpServerManager, McpServerSpec, McpTool, MessageRole, ModelPricing, PermissionMode,
PermissionPolicy, ProjectContext, PromptCacheEvent, ResolvedPermissionMode, RuntimeError,
RuntimeInvalidHookConfig, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
};
use serde::Deserialize;
use serde_json::{json, Map, Value};
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::io::{self, IsTerminal, Read, Write};
use std::net::TcpListener;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant, UNIX_EPOCH};
use tools::{
canonical_allowed_tool_name, execute_tool, mvp_tool_specs, GlobalToolRegistry,
RuntimeToolDefinition, ToolSearchOutput,
};
pub(crate) struct HookAbortMonitor {
pub(crate) stop_tx: Option<Sender<()>>,
pub(crate) join_handle: Option<JoinHandle<()>>,
}
impl HookAbortMonitor {
pub(crate) fn spawn(abort_signal: runtime::HookAbortSignal) -> Self {
Self::spawn_with_waiter(abort_signal, move |stop_rx, abort_signal| {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return;
};
runtime.block_on(async move {
let wait_for_stop = tokio::task::spawn_blocking(move || {
let _ = stop_rx.recv();
});
tokio::select! {
result = tokio::signal::ctrl_c() => {
if result.is_ok() {
abort_signal.abort();
}
}
_ = wait_for_stop => {}
}
});
})
}
pub(crate) fn spawn_with_waiter<F>(
abort_signal: runtime::HookAbortSignal,
wait_for_interrupt: F,
) -> Self
where
F: FnOnce(Receiver<()>, runtime::HookAbortSignal) + Send + 'static,
{
let (stop_tx, stop_rx) = mpsc::channel();
let join_handle = thread::spawn(move || wait_for_interrupt(stop_rx, abort_signal));
Self {
stop_tx: Some(stop_tx),
join_handle: Some(join_handle),
}
}
pub(crate) fn stop(mut self) {
if let Some(stop_tx) = self.stop_tx.take() {
let _ = stop_tx.send(());
}
if let Some(join_handle) = self.join_handle.take() {
let _ = join_handle.join();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InternalPromptProgressState {
pub(crate) command_label: &'static str,
pub(crate) task_label: String,
pub(crate) step: usize,
pub(crate) phase: String,
pub(crate) detail: Option<String>,
pub(crate) saw_final_text: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InternalPromptProgressEvent {
Started,
Update,
Heartbeat,
Complete,
Failed,
}
#[derive(Debug)]
pub(crate) struct InternalPromptProgressShared {
pub(crate) state: Mutex<InternalPromptProgressState>,
pub(crate) output_lock: Mutex<()>,
pub(crate) started_at: Instant,
}
#[derive(Debug, Clone)]
pub(crate) struct InternalPromptProgressReporter {
pub(crate) shared: Arc<InternalPromptProgressShared>,
}
#[derive(Debug)]
pub(crate) struct InternalPromptProgressRun {
pub(crate) reporter: InternalPromptProgressReporter,
pub(crate) heartbeat_stop: Option<mpsc::Sender<()>>,
pub(crate) heartbeat_handle: Option<thread::JoinHandle<()>>,
}
impl InternalPromptProgressReporter {
pub(crate) fn ultraplan(task: &str) -> Self {
Self {
shared: Arc::new(InternalPromptProgressShared {
state: Mutex::new(InternalPromptProgressState {
command_label: "Ultraplan",
task_label: task.to_string(),
step: 0,
phase: "planning started".to_string(),
detail: Some(format!("task: {task}")),
saw_final_text: false,
}),
output_lock: Mutex::new(()),
started_at: Instant::now(),
}),
}
}
pub(crate) fn emit(&self, event: InternalPromptProgressEvent, error: Option<&str>) {
let snapshot = self.snapshot();
let line = format_internal_prompt_progress_line(event, &snapshot, self.elapsed(), error);
self.write_line(&line);
}
pub(crate) fn mark_model_phase(&self) {
let snapshot = {
let mut state = self
.shared
.state
.lock()
.expect("internal prompt progress state poisoned");
state.step += 1;
state.phase = if state.step == 1 {
"analyzing request".to_string()
} else {
"reviewing findings".to_string()
};
state.detail = Some(format!("task: {}", state.task_label));
state.clone()
};
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Update,
&snapshot,
self.elapsed(),
None,
));
}
pub(crate) fn mark_tool_phase(&self, name: &str, input: &str) {
let detail = describe_tool_progress(name, input);
let snapshot = {
let mut state = self
.shared
.state
.lock()
.expect("internal prompt progress state poisoned");
state.step += 1;
state.phase = format!("running {name}");
state.detail = Some(detail);
state.clone()
};
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Update,
&snapshot,
self.elapsed(),
None,
));
}
pub(crate) fn mark_text_phase(&self, text: &str) {
let trimmed = text.trim();
if trimmed.is_empty() {
return;
}
let detail = truncate_for_summary(first_visible_line(trimmed), 120);
let snapshot = {
let mut state = self
.shared
.state
.lock()
.expect("internal prompt progress state poisoned");
if state.saw_final_text {
return;
}
state.saw_final_text = true;
state.step += 1;
state.phase = "drafting final plan".to_string();
state.detail = (!detail.is_empty()).then_some(detail);
state.clone()
};
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Update,
&snapshot,
self.elapsed(),
None,
));
}
pub(crate) fn emit_heartbeat(&self) {
let snapshot = self.snapshot();
self.write_line(&format_internal_prompt_progress_line(
InternalPromptProgressEvent::Heartbeat,
&snapshot,
self.elapsed(),
None,
));
}
pub(crate) fn snapshot(&self) -> InternalPromptProgressState {
self.shared
.state
.lock()
.expect("internal prompt progress state poisoned")
.clone()
}
pub(crate) fn elapsed(&self) -> Duration {
self.shared.started_at.elapsed()
}
pub(crate) fn write_line(&self, line: &str) {
let _guard = self
.shared
.output_lock
.lock()
.expect("internal prompt progress output lock poisoned");
let mut stdout = io::stdout();
let _ = writeln!(stdout, "{line}");
let _ = stdout.flush();
}
}
impl InternalPromptProgressRun {
pub(crate) fn start_ultraplan(task: &str) -> Self {
let reporter = InternalPromptProgressReporter::ultraplan(task);
reporter.emit(InternalPromptProgressEvent::Started, None);
let (heartbeat_stop, heartbeat_rx) = mpsc::channel();
let heartbeat_reporter = reporter.clone();
let heartbeat_handle = thread::spawn(move || loop {
match heartbeat_rx.recv_timeout(INTERNAL_PROGRESS_HEARTBEAT_INTERVAL) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => break,
Err(RecvTimeoutError::Timeout) => heartbeat_reporter.emit_heartbeat(),
}
});
Self {
reporter,
heartbeat_stop: Some(heartbeat_stop),
heartbeat_handle: Some(heartbeat_handle),
}
}
pub(crate) fn reporter(&self) -> InternalPromptProgressReporter {
self.reporter.clone()
}
pub(crate) fn finish_success(&mut self) {
self.stop_heartbeat();
self.reporter
.emit(InternalPromptProgressEvent::Complete, None);
}
pub(crate) fn finish_failure(&mut self, error: &str) {
self.stop_heartbeat();
self.reporter
.emit(InternalPromptProgressEvent::Failed, Some(error));
}
pub(crate) fn stop_heartbeat(&mut self) {
if let Some(sender) = self.heartbeat_stop.take() {
let _ = sender.send(());
}
if let Some(handle) = self.heartbeat_handle.take() {
let _ = handle.join();
}
}
}
impl Drop for InternalPromptProgressRun {
fn drop(&mut self) {
self.stop_heartbeat();
}
}
pub(crate) fn format_internal_prompt_progress_line(
event: InternalPromptProgressEvent,
snapshot: &InternalPromptProgressState,
elapsed: Duration,
error: Option<&str>,
) -> String {
let elapsed_seconds = elapsed.as_secs();
let step_label = if snapshot.step == 0 {
"current step pending".to_string()
} else {
format!("current step {}", snapshot.step)
};
let mut status_bits = vec![step_label, format!("phase {}", snapshot.phase)];
if let Some(detail) = snapshot
.detail
.as_deref()
.filter(|detail| !detail.is_empty())
{
status_bits.push(detail.to_string());
}
let status = status_bits.join(" · ");
match event {
InternalPromptProgressEvent::Started => {
format!(
"🧭 {} status · planning started · {status}",
snapshot.command_label
)
}
InternalPromptProgressEvent::Update => {
format!("{} status · {status}", snapshot.command_label)
}
InternalPromptProgressEvent::Heartbeat => format!(
"… {} heartbeat · {elapsed_seconds}s elapsed · {status}",
snapshot.command_label
),
InternalPromptProgressEvent::Complete => format!(
"✔ {} status · completed · {elapsed_seconds}s elapsed · {} steps total",
snapshot.command_label, snapshot.step
),
InternalPromptProgressEvent::Failed => format!(
"✘ {} status · failed · {elapsed_seconds}s elapsed · {}",
snapshot.command_label,
error.unwrap_or("unknown error")
),
}
}
pub(crate) struct CliHookProgressReporter;
impl runtime::HookProgressReporter for CliHookProgressReporter {
fn on_event(&mut self, event: &runtime::HookProgressEvent) {
match event {
runtime::HookProgressEvent::Started {
event,
tool_name,
command,
} => eprintln!(
"[hook {event_name}] {tool_name}: {command}",
event_name = event.as_str()
),
runtime::HookProgressEvent::Completed {
event,
tool_name,
command,
} => eprintln!(
"[hook done {event_name}] {tool_name}: {command}",
event_name = event.as_str()
),
runtime::HookProgressEvent::Cancelled {
event,
tool_name,
command,
} => eprintln!(
"[hook cancelled {event_name}] {tool_name}: {command}",
event_name = event.as_str()
),
}
}
}
pub(crate) fn first_visible_line(text: &str) -> &str {
text.lines()
.find(|line| !line.trim().is_empty())
.unwrap_or(text)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
use crate::*;
use api::{
detect_provider_kind, model_family_identity_for, resolve_startup_auth_source, AnthropicClient,
AuthSource, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest,
MessageResponse, OutputContentBlock, PromptCache, ProviderClient as ApiProviderClient,
ProviderKind, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
ToolResultContentBlock,
};
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,
render_slash_command_help_filtered, resolve_skill_invocation, resume_supported_slash_commands,
slash_command_specs, validate_slash_command_input, PluginsCommandResult, SkillSlashDispatch,
SlashCommand,
};
use init::initialize_repo;
use log::debug;
use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
use runtime::{
check_base_commit, format_stale_base_warning, format_usd, load_oauth_credentials,
load_system_prompt, load_system_prompt_with_context, pricing_for_model, resolve_expected_base,
resolve_sandbox_status, ApiClient, ApiRequest, AssistantEvent, BaseCommitState,
CompactionConfig, ConfigFileReport, ConfigLoader, ConfigSource, ContentBlock, ContextFile,
ConversationMessage, ConversationRuntime, McpConfigCollection, McpInvalidServerConfig,
McpServer, McpServerManager, McpServerSpec, McpTool, MessageRole, ModelPricing, PermissionMode,
PermissionPolicy, ProjectContext, PromptCacheEvent, ResolvedPermissionMode, RuntimeError,
RuntimeInvalidHookConfig, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
};
use serde::Deserialize;
use serde_json::{json, Map, Value};
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::io::{self, IsTerminal, Read, Write};
use std::net::TcpListener;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant, UNIX_EPOCH};
use tools::{
canonical_allowed_tool_name, execute_tool, mvp_tool_specs, GlobalToolRegistry,
RuntimeToolDefinition, ToolSearchOutput,
};
pub(crate) fn try_resolve_bare_skill_prompt(cwd: &Path, trimmed: &str) -> Option<String> {
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 {
return None;
}
match resolve_skill_invocation(cwd, Some(trimmed)) {
Ok(SkillSlashDispatch::Invoke(prompt)) => Some(prompt),
_ => None,
}
}
pub(crate) fn render_diff_report_for(cwd: &Path) -> Result<String, Box<dyn std::error::Error>> {
// Verify we are inside a git repository before calling `git diff`.
// Running `git diff --cached` outside a git tree produces a misleading
// "unknown option `cached`" error because git falls back to --no-index mode.
let in_git_repo = std::process::Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(cwd)
.output()
.is_ok_and(|o| o.status.success());
if !in_git_repo {
return Ok(format!(
"Diff\n Result no git repository\n Detail {} is not inside a git project",
cwd.display()
));
}
let staged = run_git_diff_command_in(cwd, &["diff", "--cached"])?;
let unstaged = run_git_diff_command_in(cwd, &["diff"])?;
if staged.trim().is_empty() && unstaged.trim().is_empty() {
return Ok(
"Diff\n Result clean working tree\n Detail no current changes"
.to_string(),
);
}
let mut sections = Vec::new();
if !staged.trim().is_empty() {
sections.push(format!("Staged changes:\n{}", staged.trim_end()));
}
if !unstaged.trim().is_empty() {
sections.push(format!("Unstaged changes:\n{}", unstaged.trim_end()));
}
Ok(format!("Diff\n\n{}", sections.join("\n\n")))
}
#[allow(clippy::result_large_err)]
pub(crate) fn resolve_cli_auth_source_for_cwd() -> Result<AuthSource, api::ApiError> {
resolve_startup_auth_source(|| Ok(None))
}