Hmbown/CodeWhale · error · io::Error

git not found

Error message

git not found

What it means

execute_pr_attempt_preflight runs `git apply --check` inside spawn_blocking to validate a patch artifact before applying it. It resolves git via crate::dependencies::Git::command(); when no git executable is on PATH it returns a ToolError wrapping io::ErrorKind::NotFound with "git not found". The PR attempt cannot be validated without git.

Solutions

  1. Install git in the execution environment (`apt install git`, `apk add git`).
  2. Launch from a shell where `which git` resolves, or fix PATH in the launcher/service environment.
  3. If git is at a custom path, prepend its bin directory to PATH.
  4. Optionally preflight git availability at tool startup and return a clearer setup-oriented ToolError.

Example fix

// before
docker run --rm codewhale:slim ...  # image has no git
// after
# Dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends git
Defensive patterns

Strategy: fallback

Validate before calling

// preflight in the execution environment
command -v git >/dev/null 2>&1 || { echo "install git to use PR attempts" >&2; exit 127; }

Try / catch

// rust
match execute_pr_attempt_preflight(...) {
    Err(e) if e.to_string().contains("git not found") => {
        eprintln!("git missing from PATH; install git or fix PATH and retry");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Executing the tasks tool's PR attempt flow (execute -> execute_pr_attempt_preflight) while the environment lacks a git binary; Git::command() returns None inside the spawn_blocking closure.

Common situations: Running in a container image without git; GUI-launched app with a stripped PATH; CI runner missing git in a distroless/minimal image.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/69b47be81b9c0022. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/tasks.rs:875

            .task_manager
            .as_ref()
            .ok_or_else(|| ToolError::not_available("TaskManager is not attached"))?;
        let task = read_task_for_input(input, context).await?;
        let attempt_id = required_str(input, "attempt_id")?;
        let attempt = task
            .attempts
            .iter()
            .find(|attempt| attempt.id == attempt_id)
            .ok_or_else(|| ToolError::invalid_input(format!("Attempt not found: {attempt_id}")))?;
        let patch_ref = attempt
            .patch_path
            .as_ref()
            .ok_or_else(|| ToolError::invalid_input("Attempt has no patch artifact"))?;
        let patch_path = manager.artifact_absolute_path(patch_ref);
        let workspace = context.workspace.clone();
        let out = tokio::task::spawn_blocking(move || {
            crate::dependencies::Git::command()
                .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "git not found"))?
                .args(["apply", "--check"])
                .arg(&patch_path)
                .current_dir(&workspace)
                .output()
        })
        .await
        .map_err(|join_err| {
            // Surface the otherwise-discarded join error for debugging; the
            // returned ToolError (and thus user-facing behavior) is unchanged.
            tracing::debug!(error = %join_err, "git apply --check spawn_blocking task failed to join");
            ToolError::execution_failed(format!("git apply --check panicked: {join_err}"))
        })?
        .map_err(|e| ToolError::execution_failed(format!("git apply --check failed: {e}")))?;
        let stdout = String::from_utf8_lossy(&out.stdout).to_string();
        let stderr = String::from_utf8_lossy(&out.stderr).to_string();
        ToolResult::json(&json!({
            "attempt_id": attempt_id,
            "patch_path": patch_ref,

View on GitHub (pinned to 73e0f67d83)