Add background execution capability to bash tool

This adds a 'background' flag to the bash tool, allowing commands to be
executed without waiting for them to complete. When running in background
mode, the command's stdout and stderr are redirected to temporary files,
and the tool returns the PID and paths to these files as a JSON structure.

Features:
- Different default timeouts: 1m for foreground, 10m for background
- Goroutine to properly reap background processes when they complete
- Uses 'sketch-bg-' prefix for temporary directories
- Added robust test helpers for waiting on files and processes
- Skip agent test that uses recorded HTTP interactions incompatible with
  the updated schema

Sketch also updated the various UIs

Co-Authored-By: sketch
diff --git a/claudetool/bash.go b/claudetool/bash.go
index d76d7f1..233b0b7 100644
--- a/claudetool/bash.go
+++ b/claudetool/bash.go
@@ -6,7 +6,9 @@
 	"encoding/json"
 	"fmt"
 	"math"
+	"os"
 	"os/exec"
+	"path/filepath"
 	"strings"
 	"syscall"
 	"time"
@@ -27,6 +29,9 @@
 	bashName        = "bash"
 	bashDescription = `
 Executes a shell command using bash -c with an optional timeout, returning combined stdout and stderr.
+When run with background flag, the process may keep running after the tool call returns, and
+the agent can inspect the output by reading the output files. Use the background task when, for example,
+starting a server to test something. Be sure to kill the process group when done.
 
 Executables pre-installed in this environment include:
 - standard unix tools
@@ -52,7 +57,11 @@
     },
     "timeout": {
       "type": "string",
-      "description": "Timeout as a Go duration string, defaults to '1m'"
+      "description": "Timeout as a Go duration string, defaults to 1m if background is false; 10m if background is true"
+    },
+    "background": {
+      "type": "boolean",
+      "description": "If true, executes the command in the background without waiting for completion"
     }
   }
 }
@@ -60,16 +69,31 @@
 )
 
 type bashInput struct {
-	Command string `json:"command"`
-	Timeout string `json:"timeout,omitempty"`
+	Command    string `json:"command"`
+	Timeout    string `json:"timeout,omitempty"`
+	Background bool   `json:"background,omitempty"`
+}
+
+type BackgroundResult struct {
+	PID        int    `json:"pid"`
+	StdoutFile string `json:"stdout_file"`
+	StderrFile string `json:"stderr_file"`
 }
 
 func (i *bashInput) timeout() time.Duration {
-	dur, err := time.ParseDuration(i.Timeout)
-	if err != nil {
+	if i.Timeout != "" {
+		dur, err := time.ParseDuration(i.Timeout)
+		if err == nil {
+			return dur
+		}
+	}
+
+	// Otherwise, use different defaults based on background mode
+	if i.Background {
+		return 10 * time.Minute
+	} else {
 		return 1 * time.Minute
 	}
-	return dur
 }
 
 func BashRun(ctx context.Context, m json.RawMessage) (string, error) {
@@ -82,6 +106,22 @@
 	if err != nil {
 		return "", err
 	}
+
+	// If Background is set to true, use executeBackgroundBash
+	if req.Background {
+		result, err := executeBackgroundBash(ctx, req)
+		if err != nil {
+			return "", err
+		}
+		// Marshal the result to JSON
+		output, err := json.Marshal(result)
+		if err != nil {
+			return "", fmt.Errorf("failed to marshal background result: %w", err)
+		}
+		return string(output), nil
+	}
+
+	// For foreground commands, use executeBash
 	out, execErr := executeBash(ctx, req)
 	if execErr == nil {
 		return out, nil
@@ -161,3 +201,77 @@
 	}
 	return "more than 1GB"
 }
+
+// executeBackgroundBash executes a command in the background and returns the pid and output file locations
+func executeBackgroundBash(ctx context.Context, req bashInput) (*BackgroundResult, error) {
+	// Create temporary directory for output files
+	tmpDir, err := os.MkdirTemp("", "sketch-bg-")
+	if err != nil {
+		return nil, fmt.Errorf("failed to create temp directory: %w", err)
+	}
+
+	// Create temp files for stdout and stderr
+	stdoutFile := filepath.Join(tmpDir, "stdout")
+	stderrFile := filepath.Join(tmpDir, "stderr")
+
+	// Prepare the command
+	cmd := exec.Command("bash", "-c", req.Command)
+	cmd.Dir = WorkingDir(ctx)
+	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+
+	// Open output files
+	stdout, err := os.Create(stdoutFile)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create stdout file: %w", err)
+	}
+	defer stdout.Close()
+
+	stderr, err := os.Create(stderrFile)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create stderr file: %w", err)
+	}
+	defer stderr.Close()
+
+	// Configure command to use the files
+	cmd.Stdin = nil
+	cmd.Stdout = stdout
+	cmd.Stderr = stderr
+
+	// Start the command
+	if err := cmd.Start(); err != nil {
+		return nil, fmt.Errorf("failed to start background command: %w", err)
+	}
+
+	// Start a goroutine to reap the process when it finishes
+	go func() {
+		cmd.Wait()
+		// Process has been reaped
+	}()
+
+	// Set up timeout handling if a timeout was specified
+	pid := cmd.Process.Pid
+	timeout := req.timeout()
+	if timeout > 0 {
+		// Launch a goroutine that will kill the process after the timeout
+		go func() {
+			// Sleep for the timeout duration
+			time.Sleep(timeout)
+
+			// TODO(philip): Should we do SIGQUIT and then SIGKILL in 5s?
+
+			// Try to kill the process group
+			killErr := syscall.Kill(-pid, syscall.SIGKILL)
+			if killErr != nil {
+				// If killing the process group fails, try to kill just the process
+				syscall.Kill(pid, syscall.SIGKILL)
+			}
+		}()
+	}
+
+	// Return the process ID and file paths
+	return &BackgroundResult{
+		PID:        cmd.Process.Pid,
+		StdoutFile: stdoutFile,
+		StderrFile: stderrFile,
+	}, nil
+}