Bootstrap vm run tooling before attach

Speed up first use of repo backed VMs by bootstrapping obvious tools before
the best effort LLM harness runs.

Add a host side tooling plan for pinned Go, Node, Python, and Rust versions,
summarize that plan in the uploaded prompt, and run repo mise install plus
guest global mise use -g --pin steps before the bounded opencode inspection.

Keep the harness non fatal, prefer host opencode attach when the client
supports it, fall back to guest opencode over SSH for older clients, and
cover the new flow with CLI plus planner tests.

Validation:
- go test ./internal/cli ./internal/toolingplan
- GOCACHE=/tmp/banger-gocache go test ./...
- make build
This commit is contained in:
Thales Maciel 2026-03-29 11:38:05 -03:00
parent 1e967140c3
commit 4813e844e2
No known key found for this signature in database
GPG key ID: 33112E6833C34679
10 changed files with 1126 additions and 13 deletions

View file

@ -0,0 +1,26 @@
package toolingplan
import (
"context"
"fmt"
)
type goDetector struct{}
func (goDetector) detect(_ context.Context, repoRoot string, managedTools map[string]struct{}) detectionResult {
if alreadyManaged("go", managedTools) {
return detectionResult{Skips: []SkipNote{{Target: "go", Reason: "already managed by repo mise declarations"}}}
}
goMod, ok, err := readRepoFile(repoRoot, "go.mod")
if err != nil {
return detectionResult{Skips: []SkipNote{{Target: "go", Reason: fmt.Sprintf("could not read go.mod: %v", err)}}}
}
if !ok {
return detectionResult{Skips: []SkipNote{{Target: "go", Reason: "no go.mod"}}}
}
version, ok := parseGoDirective(goMod)
if !ok {
return detectionResult{Skips: []SkipNote{{Target: "go", Reason: "go.mod has no exact go directive"}}}
}
return detectionResult{Steps: []InstallStep{{Tool: "go", Version: version, Source: "go.mod", Reason: "go directive"}}}
}

View file

@ -0,0 +1,88 @@
package toolingplan
import (
"bufio"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
toml "github.com/pelletier/go-toml"
)
func repoManagedTools(repoRoot string) (map[string]struct{}, []SkipNote) {
tools := make(map[string]struct{})
skips := make([]SkipNote, 0)
if err := collectToolVersions(filepath.Join(repoRoot, ".tool-versions"), tools); err != nil {
skips = append(skips, SkipNote{
Target: "repo mise declarations",
Reason: fmt.Sprintf("could not read .tool-versions: %v", err),
})
}
if err := collectMiseToml(filepath.Join(repoRoot, ".mise.toml"), tools); err != nil {
skips = append(skips, SkipNote{
Target: "repo mise declarations",
Reason: fmt.Sprintf("could not parse .mise.toml: %v", err),
})
}
return tools, skips
}
func collectToolVersions(path string, tools map[string]struct{}) error {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
tools[fields[0]] = struct{}{}
}
return scanner.Err()
}
func collectMiseToml(path string, tools map[string]struct{}) error {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
tree, err := toml.LoadBytes(data)
if err != nil {
return err
}
value := tree.Get("tools")
if value == nil {
return nil
}
switch typed := value.(type) {
case *toml.Tree:
for _, key := range typed.Keys() {
tools[key] = struct{}{}
}
case map[string]interface{}:
keys := make([]string, 0, len(typed))
for key := range typed {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
tools[key] = struct{}{}
}
}
return nil
}

View file

@ -0,0 +1,109 @@
package toolingplan
import (
"context"
"fmt"
"strings"
)
type nodeDetector struct{}
func (nodeDetector) detect(_ context.Context, repoRoot string, managedTools map[string]struct{}) detectionResult {
result := detectionResult{}
nodeVersion, nodeSource, nodeManaged, nodeSkip := detectNodeVersion(repoRoot, managedTools)
if nodeManaged {
result.Skips = append(result.Skips, SkipNote{Target: "node", Reason: "already managed by repo mise declarations"})
} else if nodeVersion != "" {
result.Steps = append(result.Steps, InstallStep{Tool: "node", Version: nodeVersion, Source: nodeSource, Reason: "exact runtime version"})
} else {
result.Skips = append(result.Skips, SkipNote{Target: "node", Reason: nodeSkip})
}
packageManagerVersion, packageManagerTool, packageManagerSource, packageManagerSkip := detectNodePackageManager(repoRoot)
if packageManagerTool == "" {
if packageManagerSkip != "" {
result.Skips = append(result.Skips, SkipNote{Target: "node package manager", Reason: packageManagerSkip})
}
return result
}
if alreadyManaged(packageManagerTool, managedTools) {
result.Skips = append(result.Skips, SkipNote{Target: "node package manager", Reason: packageManagerTool + " is already managed by repo mise declarations"})
return result
}
if nodeVersion == "" && !alreadyManaged("node", managedTools) {
result.Skips = append(result.Skips, SkipNote{Target: "node package manager", Reason: "packageManager is pinned but node is not pinned"})
return result
}
result.Steps = append(result.Steps, InstallStep{Tool: packageManagerTool, Version: packageManagerVersion, Source: packageManagerSource, Reason: "exact packageManager version"})
return result
}
func detectNodeVersion(repoRoot string, managedTools map[string]struct{}) (version string, source string, managed bool, skip string) {
if alreadyManaged("node", managedTools) {
return "", "", true, ""
}
for _, candidate := range []string{".node-version", ".nvmrc"} {
value, ok, err := readRepoFile(repoRoot, candidate)
if err != nil {
return "", "", false, fmt.Sprintf("could not read %s: %v", candidate, err)
}
if !ok {
continue
}
version, ok := normalizeExactVersion(strings.TrimSpace(value))
if ok {
return version, candidate, false, ""
}
return "", "", false, candidate + " does not pin an exact version"
}
packageJSON, ok, err := readRepoFile(repoRoot, "package.json")
if err != nil {
return "", "", false, fmt.Sprintf("could not read package.json: %v", err)
}
if !ok {
return "", "", false, "no pinned node version file"
}
meta, err := parsePackageJSON(packageJSON)
if err != nil {
return "", "", false, fmt.Sprintf("could not parse package.json: %v", err)
}
if version, ok := normalizeExactVersion(meta.Volta.Node); ok {
return version, "package.json#volta.node", false, ""
}
if strings.TrimSpace(meta.Volta.Node) != "" {
return "", "", false, "package.json#volta.node is not an exact version"
}
return "", "", false, "no pinned node version file"
}
func detectNodePackageManager(repoRoot string) (version string, tool string, source string, skip string) {
packageJSON, ok, err := readRepoFile(repoRoot, "package.json")
if err != nil {
return "", "", "", fmt.Sprintf("could not read package.json: %v", err)
}
if !ok {
return "", "", "", ""
}
meta, err := parsePackageJSON(packageJSON)
if err != nil {
return "", "", "", fmt.Sprintf("could not parse package.json: %v", err)
}
value := strings.TrimSpace(meta.PackageManager)
if value == "" {
return "", "", "", ""
}
parts := strings.SplitN(value, "@", 2)
if len(parts) != 2 {
return "", "", "", "packageManager is not in tool@version form"
}
tool = strings.TrimSpace(parts[0])
if tool != "pnpm" && tool != "yarn" && tool != "npm" && tool != "bun" {
return "", "", "", "packageManager is not a supported exact installer target"
}
version, ok = normalizeExactVersion(parts[1])
if !ok {
return "", "", "", "packageManager version is not exact"
}
return version, tool, "package.json#packageManager", ""
}

View file

@ -0,0 +1,94 @@
package toolingplan
import (
"context"
"os"
"path/filepath"
"sort"
)
type InstallStep struct {
Tool string
Version string
Source string
Reason string
}
type SkipNote struct {
Target string
Reason string
}
type Plan struct {
RepoManagedTools []string
Steps []InstallStep
Skips []SkipNote
}
type detector interface {
detect(context.Context, string, map[string]struct{}) detectionResult
}
type detectionResult struct {
Steps []InstallStep
Skips []SkipNote
}
var detectors = []detector{
goDetector{},
nodeDetector{},
pythonDetector{},
rustDetector{},
}
func Build(ctx context.Context, repoRoot string) Plan {
managedTools, managedSkips := repoManagedTools(repoRoot)
steps := make([]InstallStep, 0)
skips := append([]SkipNote(nil), managedSkips...)
for _, detector := range detectors {
result := detector.detect(ctx, repoRoot, managedTools)
steps = append(steps, result.Steps...)
skips = append(skips, result.Skips...)
}
sort.Slice(steps, func(i, j int) bool {
if steps[i].Tool != steps[j].Tool {
return steps[i].Tool < steps[j].Tool
}
if steps[i].Version != steps[j].Version {
return steps[i].Version < steps[j].Version
}
return steps[i].Source < steps[j].Source
})
sort.Slice(skips, func(i, j int) bool {
if skips[i].Target != skips[j].Target {
return skips[i].Target < skips[j].Target
}
return skips[i].Reason < skips[j].Reason
})
repoManagedList := make([]string, 0, len(managedTools))
for tool := range managedTools {
repoManagedList = append(repoManagedList, tool)
}
sort.Strings(repoManagedList)
return Plan{
RepoManagedTools: repoManagedList,
Steps: steps,
Skips: skips,
}
}
func readRepoFile(repoRoot, relativePath string) (string, bool, error) {
data, err := os.ReadFile(filepath.Join(repoRoot, relativePath))
if err != nil {
if os.IsNotExist(err) {
return "", false, nil
}
return "", false, err
}
return string(data), true, nil
}
func alreadyManaged(tool string, managedTools map[string]struct{}) bool {
_, ok := managedTools[tool]
return ok
}

View file

@ -0,0 +1,137 @@
package toolingplan
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
func TestBuildDetectsGoVersionFromGoMod(t *testing.T) {
repoRoot := t.TempDir()
writePlanFile(t, repoRoot, "go.mod", "module example.com/demo\n\ngo 1.25.0\n")
plan := Build(context.Background(), repoRoot)
if len(plan.Steps) != 1 {
t.Fatalf("steps = %#v, want one step", plan.Steps)
}
step := plan.Steps[0]
if step.Tool != "go" || step.Version != "1.25.0" || step.Source != "go.mod" {
t.Fatalf("step = %#v, want go@1.25.0 from go.mod", step)
}
}
func TestBuildSkipsGoWhenRepoMiseAlreadyDeclaresIt(t *testing.T) {
repoRoot := t.TempDir()
writePlanFile(t, repoRoot, ".mise.toml", "[tools]\ngo = '1.25.0'\n")
writePlanFile(t, repoRoot, "go.mod", "module example.com/demo\n\ngo 1.25.0\n")
plan := Build(context.Background(), repoRoot)
if len(plan.Steps) != 0 {
t.Fatalf("steps = %#v, want no deterministic go install", plan.Steps)
}
if !containsSkip(plan.Skips, "go", "already managed by repo mise declarations") {
t.Fatalf("skips = %#v, want managed go skip", plan.Skips)
}
if len(plan.RepoManagedTools) != 1 || plan.RepoManagedTools[0] != "go" {
t.Fatalf("repo managed tools = %#v, want [go]", plan.RepoManagedTools)
}
}
func TestBuildDetectsNodeAndPackageManager(t *testing.T) {
repoRoot := t.TempDir()
writePlanFile(t, repoRoot, ".node-version", "v22.14.0\n")
writePlanFile(t, repoRoot, "package.json", `{"packageManager":"pnpm@9.15.2"}`)
plan := Build(context.Background(), repoRoot)
if !containsStep(plan.Steps, "node", "22.14.0", ".node-version") {
t.Fatalf("steps = %#v, want node step", plan.Steps)
}
if !containsStep(plan.Steps, "pnpm", "9.15.2", "package.json#packageManager") {
t.Fatalf("steps = %#v, want pnpm step", plan.Steps)
}
}
func TestBuildSkipsPackageManagerWhenNodeIsNotPinned(t *testing.T) {
repoRoot := t.TempDir()
writePlanFile(t, repoRoot, "package.json", `{"packageManager":"pnpm@9.15.2"}`)
plan := Build(context.Background(), repoRoot)
if containsStep(plan.Steps, "pnpm", "9.15.2", "package.json#packageManager") {
t.Fatalf("steps = %#v, want no package manager install", plan.Steps)
}
if !containsSkip(plan.Skips, "node package manager", "packageManager is pinned but node is not pinned") {
t.Fatalf("skips = %#v, want node package manager skip", plan.Skips)
}
}
func TestBuildDetectsPythonAndRust(t *testing.T) {
repoRoot := t.TempDir()
writePlanFile(t, repoRoot, ".python-version", "3.12.9\n")
writePlanFile(t, repoRoot, "rust-toolchain.toml", "[toolchain]\nchannel = '1.86.0'\n")
plan := Build(context.Background(), repoRoot)
if !containsStep(plan.Steps, "python", "3.12.9", ".python-version") {
t.Fatalf("steps = %#v, want python step", plan.Steps)
}
if !containsStep(plan.Steps, "rust", "1.86.0", "rust-toolchain.toml") {
t.Fatalf("steps = %#v, want rust step", plan.Steps)
}
}
func TestBuildSkipsRustChannelNames(t *testing.T) {
repoRoot := t.TempDir()
writePlanFile(t, repoRoot, "rust-toolchain.toml", "[toolchain]\nchannel = 'stable'\n")
plan := Build(context.Background(), repoRoot)
if !containsSkip(plan.Skips, "rust", "rust-toolchain.toml channel is not an exact version") {
t.Fatalf("skips = %#v, want rust exact-version skip", plan.Skips)
}
}
func TestBuildReportsMalformedMiseTomlAsSkip(t *testing.T) {
repoRoot := t.TempDir()
writePlanFile(t, repoRoot, ".mise.toml", "[tools\nbroken")
plan := Build(context.Background(), repoRoot)
if !containsSkip(plan.Skips, "repo mise declarations", "could not parse .mise.toml") {
t.Fatalf("skips = %#v, want malformed .mise.toml skip", plan.Skips)
}
}
func writePlanFile(t *testing.T, repoRoot, relativePath, contents string) {
t.Helper()
path := filepath.Join(repoRoot, relativePath)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll(%s): %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
t.Fatalf("WriteFile(%s): %v", relativePath, err)
}
}
func containsStep(steps []InstallStep, tool, version, source string) bool {
for _, step := range steps {
if step.Tool == tool && step.Version == version && step.Source == source {
return true
}
}
return false
}
func containsSkip(skips []SkipNote, target, reasonContains string) bool {
for _, skip := range skips {
if skip.Target == target && strings.Contains(skip.Reason, reasonContains) {
return true
}
}
return false
}

View file

@ -0,0 +1,27 @@
package toolingplan
import (
"context"
"fmt"
"strings"
)
type pythonDetector struct{}
func (pythonDetector) detect(_ context.Context, repoRoot string, managedTools map[string]struct{}) detectionResult {
if alreadyManaged("python", managedTools) {
return detectionResult{Skips: []SkipNote{{Target: "python", Reason: "already managed by repo mise declarations"}}}
}
value, ok, err := readRepoFile(repoRoot, ".python-version")
if err != nil {
return detectionResult{Skips: []SkipNote{{Target: "python", Reason: fmt.Sprintf("could not read .python-version: %v", err)}}}
}
if !ok {
return detectionResult{Skips: []SkipNote{{Target: "python", Reason: "no .python-version"}}}
}
version, ok := normalizeExactVersion(strings.TrimSpace(value))
if !ok {
return detectionResult{Skips: []SkipNote{{Target: "python", Reason: ".python-version does not pin an exact version"}}}
}
return detectionResult{Steps: []InstallStep{{Tool: "python", Version: version, Source: ".python-version", Reason: "exact runtime version"}}}
}

View file

@ -0,0 +1,70 @@
package toolingplan
import (
"context"
"fmt"
"strings"
toml "github.com/pelletier/go-toml"
)
type rustDetector struct{}
func (rustDetector) detect(_ context.Context, repoRoot string, managedTools map[string]struct{}) detectionResult {
if alreadyManaged("rust", managedTools) {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: "already managed by repo mise declarations"}}}
}
if version, ok, reason := parseRustToolchainToml(repoRoot); ok {
return detectionResult{Steps: []InstallStep{{Tool: "rust", Version: version, Source: "rust-toolchain.toml", Reason: "exact toolchain channel"}}}
} else if reason != "" {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: reason}}}
}
value, ok, err := readRepoFile(repoRoot, "rust-toolchain")
if err != nil {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: fmt.Sprintf("could not read rust-toolchain: %v", err)}}}
}
if !ok {
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: "no rust-toolchain or rust-toolchain.toml"}}}
}
version := firstMeaningfulLine(value)
if normalized, ok := normalizeExactVersion(version); ok {
return detectionResult{Steps: []InstallStep{{Tool: "rust", Version: normalized, Source: "rust-toolchain", Reason: "exact toolchain channel"}}}
}
return detectionResult{Skips: []SkipNote{{Target: "rust", Reason: "rust-toolchain does not pin an exact version"}}}
}
func parseRustToolchainToml(repoRoot string) (version string, ok bool, reason string) {
data, found, err := readRepoFile(repoRoot, "rust-toolchain.toml")
if err != nil {
return "", false, fmt.Sprintf("could not read rust-toolchain.toml: %v", err)
}
if !found {
return "", false, ""
}
tree, err := toml.Load(data)
if err != nil {
return "", false, fmt.Sprintf("could not parse rust-toolchain.toml: %v", err)
}
channelValue := tree.GetDefault("toolchain.channel", "")
channel, _ := channelValue.(string)
channel = strings.TrimSpace(channel)
if channel == "" {
return "", false, "rust-toolchain.toml has no toolchain.channel"
}
version, ok = normalizeExactVersion(channel)
if !ok {
return "", false, "rust-toolchain.toml channel is not an exact version"
}
return version, true, ""
}
func firstMeaningfulLine(value string) string {
for _, line := range strings.Split(value, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
return trimmed
}
return ""
}

View file

@ -0,0 +1,41 @@
package toolingplan
import (
"encoding/json"
"regexp"
"strings"
)
var (
exactVersionPattern = regexp.MustCompile(`^v?\d+(?:\.\d+){0,2}(?:[-+][0-9A-Za-z.-]+)?$`)
goDirectivePattern = regexp.MustCompile(`(?m)^go\s+([0-9]+(?:\.[0-9]+){1,2})\s*$`)
)
func normalizeExactVersion(value string) (string, bool) {
trimmed := strings.TrimSpace(value)
if !exactVersionPattern.MatchString(trimmed) {
return "", false
}
return strings.TrimPrefix(trimmed, "v"), true
}
func parseGoDirective(goMod string) (string, bool) {
matches := goDirectivePattern.FindStringSubmatch(goMod)
if len(matches) != 2 {
return "", false
}
return matches[1], true
}
type packageJSONMetadata struct {
PackageManager string `json:"packageManager"`
Volta struct {
Node string `json:"node"`
} `json:"volta"`
}
func parsePackageJSON(data string) (packageJSONMetadata, error) {
var meta packageJSONMetadata
err := json.Unmarshal([]byte(data), &meta)
return meta, err
}