claudetool: add just-in-time tool installation for bash commands

Implements automatic tool installation when bash commands use missing tools,
providing a seamless experience for the LLM to Just Use tools it knows ought to exist.

Core Features:

1. Command Analysis Pipeline:
   - Parse bash commands to extract individual tools/programs
   - Use exec.LookPath to check if tools exist in PATH
   - Handle shell built-ins, absolute/relative paths correctly
   - Support complex command chaining with &&, ||, ;, and |

2. Subagent Tool Installation:
   - Spawn dedicated subagents to install missing tools

The system preserves existing bash tool behavior while adding invisible
tool installation. Original commands run regardless of installation
success/failure, avoiding agent confusion.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s226cd6260a6469e9k
diff --git a/claudetool/bashkit/parsing.go b/claudetool/bashkit/parsing.go
new file mode 100644
index 0000000..2b17ae3
--- /dev/null
+++ b/claudetool/bashkit/parsing.go
@@ -0,0 +1,82 @@
+package bashkit
+
+import (
+	"fmt"
+	"strings"
+
+	"mvdan.cc/sh/v3/syntax"
+)
+
+// ExtractCommands parses a bash command and extracts individual command names that are
+// candidates for auto-installation.
+//
+// Returns only simple command names (no paths, no builtins, no variable assignments)
+// that could potentially be missing tools that need installation.
+//
+// Filtering logic:
+// - Excludes commands with paths (./script.sh, /usr/bin/tool, ../build.sh)
+// - Excludes shell builtins (echo, cd, test, [, etc.)
+// - Excludes variable assignments (FOO=bar)
+// - Deduplicates repeated command names
+//
+// Examples:
+//
+//	"ls -la && echo done" → ["ls"] (echo filtered as builtin)
+//	"./deploy.sh && curl api.com" → ["curl"] (./deploy.sh filtered as path)
+//	"yamllint config.yaml" → ["yamllint"] (candidate for installation)
+func ExtractCommands(command string) ([]string, error) {
+	r := strings.NewReader(command)
+	parser := syntax.NewParser()
+	file, err := parser.Parse(r, "")
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse bash command: %w", err)
+	}
+
+	var commands []string
+	seen := make(map[string]bool)
+
+	syntax.Walk(file, func(node syntax.Node) bool {
+		callExpr, ok := node.(*syntax.CallExpr)
+		if !ok || len(callExpr.Args) == 0 {
+			return true
+		}
+		cmdName := callExpr.Args[0].Lit()
+		if cmdName == "" {
+			return true
+		}
+		if strings.Contains(cmdName, "=") {
+			// variable assignment
+			return true
+		}
+		if strings.Contains(cmdName, "/") {
+			// commands with slashes are user-specified executables/scripts
+			return true
+		}
+		if isBuiltin(cmdName) {
+			return true
+		}
+		if !seen[cmdName] {
+			seen[cmdName] = true
+			commands = append(commands, cmdName)
+		}
+		return true
+	})
+
+	return commands, nil
+}
+
+// isBuiltin checks if a command is a shell built-in using the same logic as mvdan.cc/sh/v3/interp
+// This is copied from mvdan.cc/sh/v3/interp.isBuiltin since it's not exported
+// See https://github.com/mvdan/sh/issues/1164
+func isBuiltin(name string) bool {
+	switch name {
+	case "true", ":", "false", "exit", "set", "shift", "unset",
+		"echo", "printf", "break", "continue", "pwd", "cd",
+		"wait", "builtin", "trap", "type", "source", ".", "command",
+		"dirs", "pushd", "popd", "umask", "alias", "unalias",
+		"fg", "bg", "getopts", "eval", "test", "[", "exec",
+		"return", "read", "mapfile", "readarray", "shopt":
+		return true
+	}
+	return false
+}