feat(builder): repair existing templates, a weekly check that queues repairs, and eval
Validate game templates / validate (push) Successful in 36s
Validate game templates / boot-test (push) Successful in 1m11s
Validate game templates / publish (push) Skipped

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:
gregandClaude Opus 5.5 committed 2026-10-26 00:42:34 -04:00
1 parent f11c5eaf2f
commit f1271341ef
12 files changed
+535 -20

No files matched your search

+20 -8
View File
@@ -28,6 +28,9 @@ type buildResult struct {
Summary string `json:"summary"`
// What the builder diagnosed after each failed boot, the fix it made, and how that went
Diagnoses []attempt `json:"diagnoses,omitempty"`
// Repairs only: the existing template, and whether it already booted as it is (nothing to change)
Repair bool `json:"repair,omitempty"`
NoChange bool `json:"noChange,omitempty"`
}
const defaultAgentImage = "www.gitea.gbreed.com/gbreed-public/gbr-node-agent:latest"
@@ -159,16 +162,26 @@ func buildWithImage(o buildOptions, image string) (*buildResult, error) {
return res, nil
}
// 3. Boot the draft the way nodes run it. A failure is investigated and diagnosed before the model
// changes anything; a fix that changes nothing rules its cause out
wantOutside := len(outsideDirs(report)) > 0
// 3. Boot the draft the way nodes run it
if err := bootAndFix(o, d, res, len(outsideDirs(report)) > 0); err != nil {
return res, err
}
res.Summary = buildSummary(o, res)
return res, nil
}
// bootAndFix boots the template in games/<id> the way nodes run it, until it passes. A failure is
// investigated and diagnosed before the model changes anything, and a fix that changes nothing rules its
// cause out. Builds use it on a fresh draft; repairs on an existing template, where a first boot that
// passes means there is nothing to repair. wantOutside also checks no files are lost on a restart.
func bootAndFix(o buildOptions, d *drafter, res *buildResult, wantOutside bool) error {
iv := newInvestigator(d.m, o.name, o.gamesDir, o.id, o.agentImage)
lastFailure := ""
for boot := 1; boot <= o.bootRounds; boot++ {
fmt.Printf("[build] boot %d of the draft with the node-agent\n", boot)
fmt.Printf("[build] boot %d of the template with the node-agent\n", boot)
bootProblems, notes, err := bootCheck(o.gamesDir, o.id, o.agentImage, wantOutside)
if err != nil {
return res, err
return err
}
failure := strings.Join(bootProblems, "\n")
iv.outcome(len(bootProblems) == 0, lastFailure, failure)
@@ -200,7 +213,7 @@ func buildWithImage(o buildOptions, image string) (*buildResult, error) {
problems, err := d.refine(o.attempts, fixRequest(failure, diag))
res.Rounds = d.rounds
if err != nil {
return res, err
return err
}
if len(problems) > 0 {
res.Problems = problems
@@ -211,8 +224,7 @@ func buildWithImage(o buildOptions, image string) (*buildResult, error) {
if res.Passed {
res.Problems = nil
}
res.Summary = buildSummary(o, res)
return res, nil
return nil
}
func isSteamCMDImage(image string) bool {