package main import ( "encoding/json" "errors" "flag" "fmt" "os" "path/filepath" "strings" ) // repair fixes an existing template that stopped working (a game update, a changed image), the way build // fixes a fresh draft: the template is booted as nodes run it, a failure is investigated and diagnosed, // and the model changes only what the diagnosis says. A template that boots as it is needs no repair. // The result replaces games// in a pull request. func runRepair(args []string) error { fs := flag.NewFlagSet("repair", flag.ExitOnError) gamesDir := fs.String("games", "games", "directory holding one folder per game") schemaDir := fs.String("schemas", "schema", "directory holding the JSON Schemas") id := fs.String("id", "", "the game to repair (its folder under -games)") reason := fs.String("reason", "", "why it is being repaired, e.g. the failing boot test") provider := fs.String("provider", "ollama", "ollama or gemini") modelName := fs.String("model", "", "model name (default as for draft)") attempts := fs.Int("attempts", 3, "fix rounds for validation problems, each time") bootRounds := fs.Int("boot-rounds", 5, "boots of the template at most") out := fs.String("out", "", "write the result JSON here") fs.Parse(args) if *id == "" { return errors.New("repair needs -id") } res, err := repair(buildOptions{gamesDir: *gamesDir, schemaDir: *schemaDir, id: *id, notes: *reason, provider: *provider, model: *modelName, attempts: *attempts, bootRounds: *bootRounds, agentImage: agentImageFromEnv()}) if res != nil && *out != "" { raw, _ := json.MarshalIndent(res, "", " ") if werr := os.WriteFile(*out, raw, 0o644); werr != nil && err == nil { err = werr } } if err != nil { return err } switch { case res.NoChange: fmt.Printf("[repair] %s boots as it is: nothing to repair\n", res.Dir) case res.Passed: fmt.Printf("[repair] %s repaired and booted with the node-agent\n", res.Dir) default: return fmt.Errorf("%s still has problems:\n- %s", res.Dir, strings.Join(res.Problems, "\n- ")) } return nil } func repair(o buildOptions) (*buildResult, error) { dir := filepath.Join(o.gamesDir, o.id) g, err := loadMap(filepath.Join(dir, "game.yaml")) if err != nil { return nil, err } test, _ := loadMap(filepath.Join(dir, "test.yaml")) if test == nil { test = map[string]any{} } o.name, _ = g["name"].(string) image, _ := g["defaultDockerImage"].(string) steamApp := "" if ic, ok := g["installConfig"].(map[string]any); ok && g["installMethod"] == "STEAMCMD" { steamApp = fmt.Sprint(ic["appId"]) } res := &buildResult{GameID: o.id, Dir: dir, Image: image, Repair: true} if out, err := runCmd("docker", "pull", "-q", image); err != nil { return res, fmt.Errorf("docker pull %s: %v: %s", image, err, out) } // The model starts from the template as it is, as if it had written it, with the facts read from the // image; it only ever gets asked to fix what a diagnosis found notes := "this game already has a template (your previous answer) that a node can no longer run. Repair it: " + "change only what each diagnosis says, and keep everything that works" if o.notes != "" { notes += ". Why it is being repaired: " + o.notes } d, err := newDrafter(draftJob{GamesDir: o.gamesDir, SchemaDir: o.schemaDir, Name: o.name, ID: o.id, Image: image, SteamApp: steamApp, Notes: notes, Provider: o.provider, Model: o.model, NoPull: true, Repair: true}) if err != nil { return res, err } current, err := json.Marshal(map[string]any{"game": g, "test": test}) if err != nil { return res, err } d.last = string(current) if err := bootAndFix(o, d, res, true); err != nil { return res, err } res.NoChange = res.Passed && d.rounds == 0 if res.NoChange { res.Verified = append(res.Verified, "the template boots as it is: nothing to repair") } res.Summary = repairSummary(o, res) return res, nil } // repairSummary is the pull request's description for a repair. func repairSummary(o buildOptions, res *buildResult) string { var b strings.Builder fmt.Fprintf(&b, "Repaired by the GBR AI builder. **Review before merging**: the diff shows exactly what changed.\n\n") fmt.Fprintf(&b, "- Game: **%s** (`games/%s`)\n- Image: `%s`\n- Model rounds: %d\n", o.name, o.id, res.Image, res.Rounds) if o.notes != "" { fmt.Fprintf(&b, "- Why: %s\n", o.notes) } if len(res.Verified) > 0 { b.WriteString("\n**Checked for real** (measured by the builder, not the model's word)\n") for _, v := range res.Verified { fmt.Fprintf(&b, "- %s\n", v) } } if g, err := loadMap(filepath.Join(res.Dir, "game.yaml")); err == nil { if urls := downloadURLs(g); len(urls) > 0 { b.WriteString("\n**Review carefully: nodes download from these addresses**\n") for _, u := range urls { fmt.Fprintf(&b, "- %s\n", u) } } } if len(res.Problems) > 0 { b.WriteString("\n**Still open** (the builder could not fix these)\n") for _, p := range res.Problems { fmt.Fprintf(&b, "- %s\n", firstLine(p)) } } if len(res.Diagnoses) > 0 { b.WriteString("\n**What the builder found** (after each failed boot)\n") for i, a := range res.Diagnoses { fmt.Fprintf(&b, "%d. Cause: %s\n Fix: %s\n Outcome: %s\n", i+1, firstLine(a.Cause), firstLine(a.Fix), a.Outcome) } } return b.String() }