feat(builder): repair existing templates, a weekly check that queues repairs, and eval
The builder could only create games; nothing noticed when a live template stopped working, and there was no way to tell whether a builder change made it better or worse. - repair -id ID: boots an existing template as nodes run it; if it fails, the same investigation as for a draft runs and the model changes only what the diagnosis says. The worker handles "repair" jobs (gameId from the panel) and opens "fix(games): repair ..." on draft/<id>; a template that boots as it is reports "nothing to repair" and opens no PR. The boot and fix loop is now shared by builds and repairs. - validate.yml runs weekly and boot-tests every game; failed games are sent to the panel (X-GBR-Event: games_boot_failed) to queue repairs. - eval -ids all: builds games that already have a template from scratch, blind, and scores each draft against it (boots, same image and app, ports, ready line, settings found) into eval.md and eval.json. - research skips our own sites, so neither research nor eval reads our templates back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f11c5eaf2f
commit
f1271341ef
12 files changed
+535
-20
No files matched your search
@@ -0,0 +1,140 @@
|
||||
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/<id>/ 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()
|
||||
}
|
||||
Reference in new issue
Block a user