sketch main: migrating things from /init into cmdline flags

As much as possible, I want NewAgent() to take as many arguments as it
can, and try to avoid doing different things with Init().

You can't quite do everything because the port that Docker has
open for forwarding starts as "0" and we call "docker port" to find it,
but besides that, almost everything is knowable up front.
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index 140c83d..eed68ec 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -190,6 +190,9 @@
 	dockerArgs        string
 	mounts            StringSliceFlag
 	termUI            bool
+	gitRemoteURL      string
+	commit            string
+	outsideHTTP       string
 }
 
 // parseCLIFlags parses all command-line flags and returns a CLIFlags struct
@@ -232,6 +235,10 @@
 	flag.StringVar(&flags.sketchBinaryLinux, "sketch-binary-linux", "", "(development) path to a pre-built sketch binary for linux")
 	flag.Var(&flags.experimentFlag, "x", "enable experimental features (comma-separated list or repeat flag; use 'list' to show all)")
 
+	flag.StringVar(&flags.gitRemoteURL, "git-remote-url", "", "(internal) git remote for outside sketch")
+	flag.StringVar(&flags.commit, "commit", "", "(internal) the git commit reference to check out from git remote url")
+	flag.StringVar(&flags.outsideHTTP, "outside-http", "", "(internal) host for outside sketch")
+
 	flag.Parse()
 
 	// -open's default value is not a simple true/false; it depends on other flags and conditions.
@@ -424,10 +431,13 @@
 		OutsideHostname:   flags.outsideHostname,
 		OutsideOS:         flags.outsideOS,
 		OutsideWorkingDir: flags.outsideWorkingDir,
+		WorkingDir:        wd,
 		// Ultimately this is a subtle flag because it's trying to distinguish
 		// between unsafe-on-host and inside sketch, and should probably be renamed/simplified.
-		InDocker: flags.outsideHostname != "",
-		OneShot:  flags.oneShot,
+		InDocker:      flags.outsideHostname != "",
+		OneShot:       flags.oneShot,
+		GitRemoteAddr: flags.gitRemoteURL,
+		OutsideHTTP:   flags.outsideHTTP,
 	}
 	agent := loop.NewAgent(agentConfig)
 
@@ -438,11 +448,9 @@
 	}
 
 	// Initialize the agent (only needed when not inside sketch with outside hostname)
+	// In the innie case, outtie sends a POST /init
 	if !inInsideSketch {
-		ini := loop.AgentInit{
-			WorkingDir: wd,
-		}
-		if err = agent.Init(ini); err != nil {
+		if err = agent.Init(loop.AgentInit{}); err != nil {
 			return fmt.Errorf("failed to initialize agent: %v", err)
 		}
 	}
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index 4b50ae1..c65d36d 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -90,7 +90,7 @@
 	// Initial prompt
 	Prompt string
 
-	// Initial commit to use as starting point
+	// Initial commit to use as starting point. Resolved into Commit on the host.
 	InitialCommit string
 
 	// Verbose enables verbose output
@@ -107,11 +107,20 @@
 
 	// TermUI enables terminal UI
 	TermUI bool
+
+	GitRemoteUrl string
+
+	// Commit hash to checkout from GetRemoteUrl
+	Commit string
+
+	// Outtie's HTTP server
+	OutsideHTTP string
 }
 
 // LaunchContainer creates a docker container for a project, installs sketch and opens a connection to it.
 // It writes status to stdout.
 func LaunchContainer(ctx context.Context, config ContainerConfig) error {
+	slog.Debug("Container Config", slog.String("config", fmt.Sprintf("%+v", config)))
 	if _, err := exec.LookPath("docker"); err != nil {
 		if runtime.GOOS == "darwin" {
 			return fmt.Errorf("cannot find `docker` binary; run: brew install docker colima && colima start")
@@ -198,6 +207,10 @@
 		return err
 	}
 
+	config.OutsideHTTP = fmt.Sprintf("http://sketch:%s@host.docker.internal:%s", gitSrv.pass, gitSrv.gitPort)
+	config.GitRemoteUrl = fmt.Sprintf("http://sketch:%s@host.docker.internal:%s/.git", gitSrv.pass, gitSrv.gitPort)
+	config.Commit = commit
+
 	// Create the sketch container
 	if err := createDockerContainer(ctx, cntrName, hostPort, relPath, imgName, config); err != nil {
 		return fmt.Errorf("failed to create docker container: %w", err)
@@ -337,14 +350,21 @@
 		}()
 	}
 
-	// Tell the sketch container which git server port and commit to initialize with.
+	// Tell the sketch container to Init(), which starts the SSH server
+	// and checks out the right commit.
+	// TODO: I'm trying to move as much configuration as possible into the command-line
+	// arguments to avoid splitting them up. "localAddr" is the only difficult one:
+	// we run (effectively) "docker run -p 0:80 image sketch -flags" and you can't
+	// get the port Docker chose until after the process starts. The SSH config is
+	// mostly available ahead of time, but whether it works ("sshAvailable"/"sshErrMsg")
+	// may also empirically need to be done after the SSH server is up and running.
 	go func() {
 		// TODO: Why is this called in a goroutine? I have found that when I pull this out
 		// of the goroutine and call it inline, then the terminal UI clears itself and all
 		// the scrollback (which is not good, but also not fatal).  I can't see why it does this
 		// though, since none of the calls in postContainerInitConfig obviously write to stdout
 		// or stderr.
-		if err := postContainerInitConfig(ctx, localAddr, commit, gitSrv.gitPort, gitSrv.pass, sshAvailable, sshErrMsg, sshServerIdentity, sshUserIdentity, containerCAPublicKey, hostCertificate); err != nil {
+		if err := postContainerInitConfig(ctx, localAddr, sshAvailable, sshErrMsg, sshServerIdentity, sshUserIdentity, containerCAPublicKey, hostCertificate); err != nil {
 			slog.ErrorContext(ctx, "LaunchContainer.postContainerInitConfig", slog.String("err", err.Error()))
 			errCh <- appendInternalErr(err)
 		}
@@ -514,6 +534,16 @@
 	if config.Model != "" {
 		cmdArgs = append(cmdArgs, "-model="+config.Model)
 	}
+	if config.GitRemoteUrl != "" {
+		cmdArgs = append(cmdArgs, "-git-remote-url="+config.GitRemoteUrl)
+		if config.Commit == "" {
+			panic("Commit should have been set when GitRemoteUrl was set")
+		}
+		cmdArgs = append(cmdArgs, "-commit="+config.Commit)
+	}
+	if config.OutsideHTTP != "" {
+		cmdArgs = append(cmdArgs, "-outside-http="+config.OutsideHTTP)
+	}
 	cmdArgs = append(cmdArgs, "-skaband-addr="+config.SkabandAddr)
 	if config.Prompt != "" {
 		cmdArgs = append(cmdArgs, "-prompt", config.Prompt)
@@ -623,14 +653,11 @@
 }
 
 // Contact the container and configure it.
-func postContainerInitConfig(ctx context.Context, localAddr, commit, gitPort, gitPass string, sshAvailable bool, sshError string, sshServerIdentity, sshAuthorizedKeys, sshContainerCAKey, sshHostCertificate []byte) error {
+func postContainerInitConfig(ctx context.Context, localAddr string, sshAvailable bool, sshError string, sshServerIdentity, sshAuthorizedKeys, sshContainerCAKey, sshHostCertificate []byte) error {
 	localURL := "http://" + localAddr
 
 	initMsg, err := json.Marshal(
 		server.InitRequest{
-			Commit:             commit,
-			OutsideHTTP:        fmt.Sprintf("http://sketch:%s@host.docker.internal:%s", gitPass, gitPort),
-			GitRemoteAddr:      fmt.Sprintf("http://sketch:%s@host.docker.internal:%s/.git", gitPass, gitPort),
 			HostAddr:           localAddr,
 			SSHAuthorizedKeys:  sshAuthorizedKeys,
 			SSHServerIdentity:  sshServerIdentity,
diff --git a/loop/agent.go b/loop/agent.go
index c786477..e26bad0 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -744,10 +744,18 @@
 	InDocker         bool
 	UseAnthropicEdit bool
 	OneShot          bool
+	WorkingDir       string
 	// Outside information
 	OutsideHostname   string
 	OutsideOS         string
 	OutsideWorkingDir string
+
+	// Outtie's HTTP to, e.g., open a browser
+	OutsideHTTP string
+	// Outtie's Git server
+	GitRemoteAddr string
+	// Commit to checkout from Outtie
+	Commit string
 }
 
 // NewAgent creates a new Agent.
@@ -767,19 +775,18 @@
 		outstandingLLMCalls:  make(map[string]struct{}),
 		outstandingToolCalls: make(map[string]string),
 		stateMachine:         NewStateMachine(),
+		workingDir:           config.WorkingDir,
+		outsideHTTP:          config.OutsideHTTP,
+		gitRemoteAddr:        config.GitRemoteAddr,
 	}
 	return agent
 }
 
 type AgentInit struct {
-	WorkingDir string
-	NoGit      bool // only for testing
+	NoGit bool // only for testing
 
-	InDocker      bool
-	Commit        string
-	OutsideHTTP   string
-	GitRemoteAddr string
-	HostAddr      string
+	InDocker bool
+	HostAddr string
 }
 
 func (a *Agent) Init(ini AgentInit) error {
@@ -787,12 +794,11 @@
 		return fmt.Errorf("Agent.Init: already initialized")
 	}
 	ctx := a.config.Context
-	if ini.InDocker && ini.Commit != "" {
-		if err := setupGitHooks(ini.WorkingDir); err != nil {
-			slog.WarnContext(ctx, "failed to set up git hooks", "err", err)
-		}
+
+	// Fetch, if so configured.
+	if ini.InDocker && a.config.Commit != "" && a.config.GitRemoteAddr != "" {
 		cmd := exec.CommandContext(ctx, "git", "stash")
-		cmd.Dir = ini.WorkingDir
+		cmd.Dir = a.workingDir
 		if out, err := cmd.CombinedOutput(); err != nil {
 			return fmt.Errorf("git stash: %s: %v", out, err)
 		}
@@ -800,53 +806,51 @@
 		// it runs "git fetch" underneath the covers to get its latest commits. By configuring
 		// an additional remote.sketch-host.fetch, we make "origin/main" on innie sketch look like
 		// origin/main on outtie sketch, which should make it easier to rebase.
-		cmd = exec.CommandContext(ctx, "git", "remote", "add", "sketch-host", ini.GitRemoteAddr)
-		cmd.Dir = ini.WorkingDir
+		cmd = exec.CommandContext(ctx, "git", "remote", "add", "sketch-host", a.gitRemoteAddr)
+		cmd.Dir = a.workingDir
 		if out, err := cmd.CombinedOutput(); err != nil {
 			return fmt.Errorf("git remote add: %s: %v", out, err)
 		}
 		cmd = exec.CommandContext(ctx, "git", "config", "--add", "remote.sketch-host.fetch",
 			"+refs/heads/feature/*:refs/remotes/origin/feature/*")
-		cmd.Dir = ini.WorkingDir
+		cmd.Dir = a.workingDir
 		if out, err := cmd.CombinedOutput(); err != nil {
 			return fmt.Errorf("git config --add: %s: %v", out, err)
 		}
 		cmd = exec.CommandContext(ctx, "git", "fetch", "--prune", "sketch-host")
-		cmd.Dir = ini.WorkingDir
+		cmd.Dir = a.workingDir
 		if out, err := cmd.CombinedOutput(); err != nil {
 			return fmt.Errorf("git fetch: %s: %w", out, err)
 		}
-		cmd = exec.CommandContext(ctx, "git", "checkout", "-f", ini.Commit)
-		cmd.Dir = ini.WorkingDir
+		cmd = exec.CommandContext(ctx, "git", "checkout", "-f", a.config.Commit)
+		cmd.Dir = a.workingDir
 		if checkoutOut, err := cmd.CombinedOutput(); err != nil {
 			// Remove git hooks if they exist and retry
 			// Only try removing hooks if we haven't already removed them during fetch
-			hookPath := filepath.Join(ini.WorkingDir, ".git", "hooks")
+			hookPath := filepath.Join(a.workingDir, ".git", "hooks")
 			if _, statErr := os.Stat(hookPath); statErr == nil {
 				slog.WarnContext(ctx, "git checkout failed, removing hooks and retrying",
 					slog.String("error", err.Error()),
 					slog.String("output", string(checkoutOut)))
-				if removeErr := removeGitHooks(ctx, ini.WorkingDir); removeErr != nil {
+				if removeErr := removeGitHooks(ctx, a.workingDir); removeErr != nil {
 					slog.WarnContext(ctx, "failed to remove git hooks", slog.String("error", removeErr.Error()))
 				}
 
 				// Retry the checkout operation
-				cmd = exec.CommandContext(ctx, "git", "checkout", "-f", ini.Commit)
-				cmd.Dir = ini.WorkingDir
+				cmd = exec.CommandContext(ctx, "git", "checkout", "-f", a.config.Commit)
+				cmd.Dir = a.workingDir
 				if retryOut, retryErr := cmd.CombinedOutput(); retryErr != nil {
-					return fmt.Errorf("git checkout %s failed even after removing hooks: %s: %w", ini.Commit, retryOut, retryErr)
+					return fmt.Errorf("git checkout %s failed even after removing hooks: %s: %w", a.config.Commit, retryOut, retryErr)
 				}
 			} else {
-				return fmt.Errorf("git checkout %s: %s: %w", ini.Commit, checkoutOut, err)
+				return fmt.Errorf("git checkout %s: %s: %w", a.config.Commit, checkoutOut, err)
 			}
 		}
-		a.gitRemoteAddr = ini.GitRemoteAddr
-		a.outsideHTTP = ini.OutsideHTTP
-		if ini.HostAddr != "" {
-			a.url = "http://" + ini.HostAddr
-		}
 	}
-	a.workingDir = ini.WorkingDir
+
+	if ini.HostAddr != "" {
+		a.url = "http://" + ini.HostAddr
+	}
 
 	if !ini.NoGit {
 		repoRoot, err := repoRoot(ctx, a.workingDir)
@@ -859,12 +863,15 @@
 			return fmt.Errorf("resolveRef: %w", err)
 		}
 
+		if err := setupGitHooks(a.workingDir); err != nil {
+			slog.WarnContext(ctx, "failed to set up git hooks", "err", err)
+		}
+
 		cmd := exec.CommandContext(ctx, "git", "tag", "-f", a.SketchGitBaseRef(), "HEAD")
 		cmd.Dir = repoRoot
 		if out, err := cmd.CombinedOutput(); err != nil {
 			return fmt.Errorf("git tag -f %s %s: %s: %w", a.SketchGitBaseRef(), "HEAD", out, err)
 		}
-		a.lastHEAD = ini.Commit
 
 		slog.Info("running codebase analysis")
 		codebase, err := onstart.AnalyzeCodebase(ctx, a.repoRoot)
@@ -879,7 +886,7 @@
 		}
 		a.codereview = codereview
 
-		a.gitOrigin = getGitOrigin(ctx, ini.WorkingDir)
+		a.gitOrigin = getGitOrigin(ctx, a.workingDir)
 	}
 	a.lastHEAD = a.SketchGitBase()
 	a.convo = a.initConvo()
@@ -1787,7 +1794,7 @@
 	cmd.Dir = dir
 	out, err := cmd.Output()
 	if err != nil {
-		return "", fmt.Errorf("git rev-parse failed: %w\n%s", err, stderr)
+		return "", fmt.Errorf("git rev-parse (in %s) failed: %w\n%s", dir, err, stderr)
 	}
 	return strings.TrimSpace(string(out)), nil
 }
diff --git a/loop/agent_test.go b/loop/agent_test.go
index 31e9664..bf9d6a9 100644
--- a/loop/agent_test.go
+++ b/loop/agent_test.go
@@ -68,7 +68,8 @@
 
 	apiKey := cmp.Or(os.Getenv("OUTER_SKETCH_MODEL_API_KEY"), os.Getenv("ANTHROPIC_API_KEY"))
 	cfg := AgentConfig{
-		Context: ctx,
+		Context:    ctx,
+		WorkingDir: wd,
 		Service: &ant.Service{
 			APIKey: apiKey,
 			HTTPC:  client,
@@ -84,7 +85,7 @@
 	if err := os.Chdir(origWD); err != nil {
 		t.Fatal(err)
 	}
-	err = agent.Init(AgentInit{WorkingDir: wd, NoGit: true})
+	err = agent.Init(AgentInit{NoGit: true})
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 07127df..3897fb9 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -85,10 +85,10 @@
 }
 
 type InitRequest struct {
-	HostAddr           string `json:"host_addr"`
-	OutsideHTTP        string `json:"outside_http"`
-	GitRemoteAddr      string `json:"git_remote_addr"`
-	Commit             string `json:"commit"`
+	// Passed to agent so that the URL it prints in the termui prompt is correct (when skaband is not used)
+	HostAddr string `json:"host_addr"`
+
+	// POST /init will start the SSH server with these configs
 	SSHAuthorizedKeys  []byte `json:"ssh_authorized_keys"`
 	SSHServerIdentity  []byte `json:"ssh_server_identity"`
 	SSHContainerCAKey  []byte `json:"ssh_container_ca_key"`
@@ -215,12 +215,8 @@
 		}
 
 		ini := loop.AgentInit{
-			WorkingDir:    "/app",
-			InDocker:      true,
-			Commit:        m.Commit,
-			OutsideHTTP:   m.OutsideHTTP,
-			GitRemoteAddr: m.GitRemoteAddr,
-			HostAddr:      m.HostAddr,
+			InDocker: true,
+			HostAddr: m.HostAddr,
 		}
 		if err := agent.Init(ini); err != nil {
 			http.Error(w, "init failed: "+err.Error(), http.StatusInternalServerError)