diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index 42686016bb..d6d8217f46 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -257,7 +257,9 @@ pub fn detect_provider_kind(model: &str) -> ProviderKind { pub const fn model_family_identity_for_kind(kind: ProviderKind) -> runtime::ModelFamilyIdentity { match kind { ProviderKind::Anthropic => runtime::ModelFamilyIdentity::Claude, - ProviderKind::Xai | ProviderKind::OpenAi => runtime::ModelFamilyIdentity::Generic, + ProviderKind::Xai | ProviderKind::OpenAi | ProviderKind::Ollama => { + runtime::ModelFamilyIdentity::Generic + } } } diff --git a/rust/crates/runtime/src/session_control.rs b/rust/crates/runtime/src/session_control.rs index 880e5e67d5..8ea72f67bf 100644 --- a/rust/crates/runtime/src/session_control.rs +++ b/rust/crates/runtime/src/session_control.rs @@ -31,7 +31,10 @@ impl SessionStore { /// The on-disk layout becomes `/.hackcode/sessions//`. pub fn from_cwd(cwd: impl AsRef) -> Result { let cwd = cwd.as_ref(); - let sessions_root = cwd + // #151: canonicalize cwd for consistent fingerprinting across + // equivalent path representations. + let canonical_cwd = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf()); + let sessions_root = canonical_cwd .join(".hackcode") .join("sessions") .join(workspace_fingerprint(&canonical_cwd)); diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index cbcfcc9a4f..e448b18723 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -155,6 +155,7 @@ error: {message} Run `hackcode --help` for usage." ); } + } std::process::exit(1); } } @@ -391,7 +392,10 @@ fn run() -> Result<(), Box> { allow_broad_cwd, )?; } - CliAction::HelpTopic(topic) => print_help_topic(topic), + CliAction::HelpTopic { + topic, + output_format, + } => print_help_topic(topic, output_format)?, CliAction::Help { output_format } => print_help(output_format)?, CliAction::Setup => setup::run_setup()?, CliAction::Scan => { @@ -502,7 +506,10 @@ enum CliAction { reasoning_effort: Option, allow_broad_cwd: bool, }, - HelpTopic(LocalHelpTopic), + HelpTopic { + topic: LocalHelpTopic, + output_format: CliOutputFormat, + }, Setup, Scan, Update, @@ -6131,6 +6138,20 @@ fn collect_sessions_from_dir( if !directory.exists() { return Ok(()); } + // #148: classify the workspace lifecycle once (saved-only / idle-shell / + // running-process, plus dirty-worktree and abandoned flags) and share it + // across the sessions in this store — they all belong to the same + // workspace-fingerprinted directory. + let lifecycle = env::current_dir() + .map(|cwd| classify_session_lifecycle_for(&cwd)) + .unwrap_or(SessionLifecycleSummary { + kind: SessionLifecycleKind::SavedOnly, + pane_id: None, + pane_command: None, + pane_path: None, + workspace_dirty: false, + abandoned: false, + }); for entry in fs::read_dir(directory)? { let entry = entry?; let path = entry.path(); @@ -6180,6 +6201,7 @@ fn collect_sessions_from_dir( message_count, parent_session_id, branch_name, + lifecycle: lifecycle.clone(), }); } Ok(()) @@ -6426,6 +6448,82 @@ fn print_status_snapshot( Ok(()) } +/// #148: where the resolved model string originated from. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ModelSource { + Flag, + Env, + Config, + Default, +} + +impl ModelSource { + fn as_str(self) -> &'static str { + match self { + ModelSource::Flag => "flag", + ModelSource::Env => "env", + ModelSource::Config => "config", + ModelSource::Default => "default", + } + } +} + +/// #148: provenance of the model selection — the resolved (alias-expanded) +/// name, the raw user/env input before resolution, and which source won. +#[derive(Debug, Clone)] +struct ModelProvenance { + resolved: String, + raw: Option, + source: ModelSource, +} + +impl ModelProvenance { + /// Probe the model-selection env vars; attribute to env when one is set, + /// otherwise treat the resolved model as a built-in default. + fn from_env_or_config_or_default(model: &str) -> Self { + for key in ["HACKCODE_MODEL", "CLAW_MODEL", "ANTHROPIC_MODEL"] { + if let Ok(value) = std::env::var(key) { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Self { + resolved: model.to_string(), + raw: Some(trimmed.to_string()), + source: ModelSource::Env, + }; + } + } + } + Self { + resolved: model.to_string(), + raw: None, + source: ModelSource::Default, + } + } +} + +/// #148: compute the stale-base commit state for `cwd`, honoring an optional +/// `--base-commit` flag value (falls back to the `.hackcode-base` file). +fn stale_base_state_for(cwd: &Path, flag_value: Option<&str>) -> BaseCommitState { + let source = resolve_expected_base(flag_value, cwd); + check_base_commit(cwd, source.as_ref()) +} + +/// #148: render a [`BaseCommitState`] as a JSON object with a stable `status` +/// token and a `fresh` boolean (false only when the worktree has diverged). +fn stale_base_json_value(state: &BaseCommitState) -> serde_json::Value { + match state { + BaseCommitState::Matches => json!({ "status": "matches", "fresh": true }), + BaseCommitState::NoExpectedBase => json!({ "status": "no_expected_base", "fresh": true }), + BaseCommitState::NotAGitRepo => json!({ "status": "not_a_git_repo", "fresh": true }), + BaseCommitState::Diverged { expected, actual } => json!({ + "status": "diverged", + "fresh": false, + "expected": expected, + "actual": actual, + }), + } +} + fn status_json_value( model: Option<&str>, usage: StatusUsage, diff --git a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs index bc4fbe2c90..d6fb3f9644 100644 --- a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs +++ b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs @@ -15,7 +15,7 @@ fn status_command_applies_model_and_permission_mode_flags() { fs::create_dir_all(&temp_dir).expect("temp dir should exist"); // when - let output = Command::new(env!("CARGO_BIN_EXE_claw")) + let output = Command::new(env!("CARGO_BIN_EXE_hackcode")) .current_dir(&temp_dir) .args([ "--model", @@ -45,7 +45,7 @@ fn resume_flag_loads_a_saved_session_and_dispatches_status() { let session_path = write_session(&temp_dir, "resume-status"); // when - let output = Command::new(env!("CARGO_BIN_EXE_claw")) + let output = Command::new(env!("CARGO_BIN_EXE_hackcode")) .current_dir(&temp_dir) .args([ "--resume", @@ -73,12 +73,12 @@ fn slash_command_names_match_known_commands_and_suggest_nearby_unknown_ones() { fs::create_dir_all(&temp_dir).expect("temp dir should exist"); // when - let help_output = Command::new(env!("CARGO_BIN_EXE_claw")) + let help_output = Command::new(env!("CARGO_BIN_EXE_hackcode")) .current_dir(&temp_dir) .arg("/help") .output() .expect("claw should launch"); - let unknown_output = Command::new(env!("CARGO_BIN_EXE_claw")) + let unknown_output = Command::new(env!("CARGO_BIN_EXE_hackcode")) .current_dir(&temp_dir) .arg("/zstats") .output() @@ -109,7 +109,7 @@ fn omc_namespaced_slash_commands_surface_a_targeted_compatibility_hint() { let temp_dir = unique_temp_dir("slash-dispatch-omc"); fs::create_dir_all(&temp_dir).expect("temp dir should exist"); - let output = Command::new(env!("CARGO_BIN_EXE_claw")) + let output = Command::new(env!("CARGO_BIN_EXE_hackcode")) .current_dir(&temp_dir) .arg("/oh-my-claudecode:hud") .output() @@ -259,7 +259,7 @@ fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() { } fn command_in(cwd: &Path) -> Command { - let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); + let mut command = Command::new(env!("CARGO_BIN_EXE_hackcode")); command.current_dir(cwd); command } diff --git a/rust/crates/rusty-claude-cli/tests/compact_output.rs b/rust/crates/rusty-claude-cli/tests/compact_output.rs index 4ccca2f47b..e504743882 100644 --- a/rust/crates/rusty-claude-cli/tests/compact_output.rs +++ b/rust/crates/rusty-claude-cli/tests/compact_output.rs @@ -250,7 +250,7 @@ fn run_claw( base_url: &str, args: &[&str], ) -> Output { - let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); + let mut command = Command::new(env!("CARGO_BIN_EXE_hackcode")); command .current_dir(cwd) .env_clear() diff --git a/rust/crates/rusty-claude-cli/tests/compact_repl_panic.rs b/rust/crates/rusty-claude-cli/tests/compact_repl_panic.rs index e930cf4370..9e0f2c7f79 100644 --- a/rust/crates/rusty-claude-cli/tests/compact_repl_panic.rs +++ b/rust/crates/rusty-claude-cli/tests/compact_repl_panic.rs @@ -55,7 +55,7 @@ fn run_claw_repl( home: &std::path::Path, stdin: &str, ) -> Output { - let mut command = python_pty_command(env!("CARGO_BIN_EXE_claw")); + let mut command = python_pty_command(env!("CARGO_BIN_EXE_hackcode")); let mut child = command .current_dir(cwd) .env_clear() diff --git a/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs b/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs index 066abb686b..0a25e4b630 100644 --- a/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs +++ b/rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs @@ -308,7 +308,7 @@ struct ScenarioReport { } fn run_case(case: ScenarioCase, workspace: &HarnessWorkspace, base_url: &str) -> ScenarioRun { - let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); + let mut command = Command::new(env!("CARGO_BIN_EXE_hackcode")); command .current_dir(&workspace.root) .env_clear() diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 8ed644f5a6..2bee22f7f8 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -586,7 +586,7 @@ fn assert_json_command_with_env(current_dir: &Path, args: &[&str], envs: &[(&str } fn run_claw(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output { - let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); + let mut command = Command::new(env!("CARGO_BIN_EXE_hackcode")); command.current_dir(current_dir).args(args); for (key, value) in envs { command.env(key, value); diff --git a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs index a035544f4b..0b3e7f3e4c 100644 --- a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs +++ b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs @@ -543,7 +543,7 @@ fn workspace_session(root: &Path) -> Session { } fn run_claw_with_env(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output { - let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); + let mut command = Command::new(env!("CARGO_BIN_EXE_hackcode")); command.current_dir(current_dir).args(args); for (key, value) in envs { command.env(key, value);