"""Agentic build instrument — "build a real small app" end-to-end. Given a plan and a thin starter scaffold, the agent uses tools to write ~8 files, get `go build` green, get `go test` green, and produce a binary that starts, serves an HTML form, and persists a POSTed note to SQLite. The four "real product" gates are run by the harness, not the agent. This is a Go + SQLite + html/template app — a realistic small self-hosted web tool in miniature. It tests the actual workload: multi-turn tool-use, file creation, dependency management, iterative build/test cycles. Usage (via the bench.py CLI): python bench.py agentic-build validate python bench.py agentic-build run Env (via .env): ENDPOINT, MODEL, API_KEY, MAX_TOKENS, MAX_TURNS. """ import json import os import re import sys import time import shutil import subprocess import socket import signal import urllib.request import urllib.parse import urllib.error from bench.core import client from bench.core import tools from bench.core.reporting import write_json HERE = os.path.dirname(os.path.abspath(__file__)) WORK = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "work", "agentic_build") MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "16000")) MAX_TURNS = int(os.environ.get("MAX_TURNS", "40")) BUILD_TIMEOUT = int(os.environ.get("BUILD_TIMEOUT", "180")) TEST_TIMEOUT = int(os.environ.get("TEST_TIMEOUT", "180")) SERVER_TIMEOUT = int(os.environ.get("SERVER_TIMEOUT", "20")) PLAN = """\ # Task — Build a Go web binary with a SQLite-backed notes CRUD ## Goal `go build ./...` and `go test ./...` run green in the work dir, and the resulting binary starts on port 8080, serves an HTML form at `/` that lists existing notes and lets a visitor create a new one, and persists created notes to an embedded SQLite file `notes.db`. ## Prerequisites A starter scaffold is already in the work dir: - `go.mod` (module `notes`, go 1.26, no deps yet) - `cmd/server/main.go` (empty stub: `package main; func main(){}`) You start from this scaffold. Do not delete existing files; build on them. ## Spec — the app to build A single Go binary that serves a one-page HTML UI over HTTP. One `cmd/`, modular `internal/` packages, server-rendered `html/template`, embedded assets, embedded SQLite via the pure-Go driver `modernc.org/sqlite`. ### Files to produce (layout) cmd/server/main.go boot: config -> db -> migrate -> handlers -> ListenAndServe + graceful shutdown internal/config/config.go Config{Port, DBPath}; LoadFromEnv() with sane defaults (port 8080, db "notes.db"). MUST read env vars NOTES_PORT (port) and NOTES_DB (db path) — the scoring harness sets these. internal/db/db.go Open(dsn): open modernc sqlite, PRAGMA WAL + busy_timeout + foreign_keys ON; Migrate() via embedded .sql internal/db/migrations/00001_init.sql CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT NOT NULL, body TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP); internal/models/note.go Note struct + Create(db, title, body) + List(db) + Delete(db, id); plain database/sql, no ORM internal/handlers/handlers.go Handlers struct; routes via stdlib net/http ServeMux: GET / (list+form), POST / (create), POST /{id}/delete (delete) internal/handlers/handlers_test.go httptest: POST a note, GET /, assert the note title appears in the response body web/templates/index.html the one page: a form (title + body + submit) above a list of notes (each with a delete button); rendered server-side by html/template ### Constraints - Use `modernc.org/sqlite` (pure-Go, CGO_ENABLED=0). Import path is `_ "modernc.org/sqlite"`. - Embed the migration SQL and the templates via `//go:embed`. - Stdlib `net/http` ServeMux only — no Gin, no chi. - No JS framework. Plain HTML form posts back to the server. ## Steps 1. Add the dependency: `go get modernc.org/sqlite`. Run `go mod tidy`. 2. Write the files. Natural order: config -> db -> models -> handlers -> templates -> main. 3. Build (`go build ./...`). Fix any compile errors. 4. Test (`go test ./...`). The handlers_test.go must pass. 5. Optionally run the binary briefly to confirm it serves. ## Acceptance (harness-verified) 1. `go build ./...` exits 0. 2. `go test ./...` exits 0. 3. The built binary starts on port 8080 and `GET /` returns HTTP 200 with a `
{{range .}}{{.Body}}
No notes yet.
{{end}}''', "internal/handlers/handlers_test.go": '''package handlers import ("database/sql"; "io"; "net/http"; "net/http/httptest"; "net/url"; "strings"; "testing"; "notes/internal/models"; _ "modernc.org/sqlite") func newTestStore(t *testing.T) (*models.Store, func()) { t.Helper() db, err := sql.Open("sqlite", ":memory:") if err != nil { t.Fatalf("open: %v", err) } _, err = db.Exec(`CREATE TABLE notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, body TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)`) if err != nil { db.Close(); t.Fatalf("create table: %v", err) } return &models.Store{DB: db}, func() { db.Close() } } func TestCreateThenList(t *testing.T) { store, cleanup := newTestStore(t); defer cleanup() h := New(store) srv := httptest.NewServer(h.Routes()); defer srv.Close() form := url.Values{"title": {"My Note"}, "body": {"First body"}} resp, err := http.PostForm(srv.URL+"/", form) if err != nil { t.Fatalf("post: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("post status = %d, want 200", resp.StatusCode) } resp2, err := http.Get(srv.URL + "/") if err != nil { t.Fatalf("get: %v", err) } defer resp2.Body.Close() body, _ := io.ReadAll(resp2.Body) if !strings.Contains(string(body), "My Note") { t.Fatalf("response does not contain the created note title; got:\\n%s", body) } } ''', "internal/handlers/doc.go": "package handlers", "internal/models/doc.go": "package models", "internal/config/doc.go": "package config", "internal/db/doc.go": "package db", } # ----------------------------------------------------------------- scoring gates def _has_real_app(work_dir): main_go = os.path.join(work_dir, "cmd", "server", "main.go") if not os.path.isfile(main_go): return False with open(main_go) as f: src = f.read() if "ListenAndServe" not in src and "http.Server" not in src: return False internal = os.path.join(work_dir, "internal") if not os.path.isdir(internal): return False pkg_dirs = [d for d in os.listdir(internal) if os.path.isdir(os.path.join(internal, d))] return len(pkg_dirs) >= 3 def _run(cmd, work_dir, timeout): try: p = subprocess.run(cmd, cwd=work_dir, shell=True, capture_output=True, text=True, timeout=timeout) except subprocess.TimeoutExpired: return f"TIMEOUT after {timeout}s" return f"exit={p.returncode}\n{(p.stdout + p.stderr).strip()}" def _free_port(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind(("", 0)) p = s.getsockname()[1]; s.close(); return p def gates(work_dir): g = {} if not _has_real_app(work_dir): return {"build": False, "test": False, "serves": False, "persists": False} b = _run("go build ./...", work_dir, BUILD_TIMEOUT) g["build"] = b.startswith("exit=0") t = _run("go test ./...", work_dir, TEST_TIMEOUT) g["test"] = t.startswith("exit=0") binpath = os.path.abspath(os.path.join(work_dir, "server")) r = _run(f"go build -o {binpath} ./cmd/server", work_dir, BUILD_TIMEOUT) if not r.startswith("exit=0") or not os.path.isfile(binpath): g["serves"] = False; g["persists"] = False; return g port = _free_port() dbpath = os.path.abspath(os.path.join(work_dir, f"bench_{port}.db")) for ext in ("", "-wal", "-shm"): if os.path.exists(dbpath + ext): os.remove(dbpath + ext) env = dict(os.environ, NOTES_PORT=str(port), NOTES_DB=dbpath) try: proc = subprocess.Popen([binpath], cwd=work_dir, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except Exception: g["serves"] = False; g["persists"] = False; return g try: for _ in range(40): try: with socket.create_connection(("127.0.0.1", port), 0.25): break except OSError: time.sleep(0.25) else: g["serves"] = False; g["persists"] = False; return g base = f"http://127.0.0.1:{port}" try: with urllib.request.urlopen(base + "/", timeout=SERVER_TIMEOUT) as resp: if resp.status != 200: g["serves"] = False; g["persists"] = False; return g html = resp.read().decode(errors="replace") except Exception: g["serves"] = False; g["persists"] = False; return g g["serves"] = "