feat: post-build pipeline (review, fix, security round, readability) and bakeoff record

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev d23b6edf70eb129b03c865ea2faf4802036455d4 parent 4ceb42e
6 files changed, +96 −12
  • builders/post-build.sh +45 −0
  • builders/run-readability.sh +1 −1
  • harness/cmd/bakeoff/main.go +12 −0
  • harness/harness_test.go +8 −0
  • harness/results.go +19 −0
  • results/go.json +11 −11
diff --git a/builders/post-build.sh b/builders/post-build.sh
new file mode 100755
index 0000000..bc072dd
--- /dev/null
+++ b/builders/post-build.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Everything after a build, identically for every app: tokens, live suite,
+# Astra review, one fix round, re-review, a security-only round if the
+# re-review still finds security problems (then a final review, record
+# only), and Fable's blind readability score.
+# builders/post-build.sh <slug>
+set -uo pipefail
+slug=$1
+repo=$(cd "$(dirname "$0")/.." && pwd)
+cd "$repo"
+app="$repo/.worktrees/$slug/apps/$slug"
+status=$(head -1 "$app/STATUS.md" 2>/dev/null || echo "BLOCKED no STATUS.md written")
+bakeoff tokens --slug "$slug" --phase build
+lang=$(cd harness && go run ./cmd/language "$slug")
+case $status in
+ LIVE*) ;;
+ *) bakeoff record --slug "$slug" --language "$lang" --status "$status"; echo "$slug: $status"; exit 0 ;;
+esac
+read -r _ url version <<<"$status"
+bakeoff record --slug "$slug" --language "$lang" --status LIVE --app-url "$url" --version "$version"
+suite() { scripts/test-app.sh "$url" "$TMPDIR/$slug-suite.json" >/dev/null 2>&1; bakeoff behaviour --slug "$slug" --file "$TMPDIR/$slug-suite.json"; rm -f "$TMPDIR/$slug-suite.json"; }
+suite
+builders/run-review.sh "$slug" 1
+if [ "$(tr -d '[:space:]' < "builders/reviews/$slug.1.md")" != "Nofindings." ]; then
+ builders/run-fix.sh "$slug" fix "builders/reviews/$slug.1.md"
+ bakeoff tokens --slug "$slug" --phase fix
+fi
+builders/run-review.sh "$slug" 2
+last=2
+if ! bakeoff findings --slug "$slug" --file "builders/reviews/$slug.2.md"; then
+ grep -E '^\s*[0-9]+\.\s+\[security\]' -A20 "builders/reviews/$slug.2.md" | awk '/^\s*[0-9]+\.\s+\[/{keep=/\[security\]/} keep' > "$TMPDIR/$slug.security.md"
+ builders/run-fix.sh "$slug" security "$TMPDIR/$slug.security.md"
+ rm -f "$TMPDIR/$slug.security.md"
+ bakeoff tokens --slug "$slug" --phase security
+ builders/run-review.sh "$slug" 3
+ last=3
+ bakeoff findings --slug "$slug" --file "builders/reviews/$slug.3.md"
+fi
+bakeoff tokens --slug "$slug" --phase review
+status=$(head -1 "$app/STATUS.md")
+read -r _ url version <<<"$status"
+bakeoff record --slug "$slug" --version "$version"
+suite
+APP_DIR="$app" builders/run-readability.sh "$slug"
+echo "$slug: post-build done (reviews: $last)"
diff --git a/builders/run-readability.sh b/builders/run-readability.sh
index 902649f..e8362e3 100755
--- a/builders/run-readability.sh
+++ b/builders/run-readability.sh
@@ -12,7 +12,7 @@ trap 'rm -rf "$work"' EXIT
{
sed "s/{LANGUAGE}/$language/g" builders/prompts/readability.md
echo
- bakeoff source --slug "$slug" | sed -e "s/todo-$slug/todo-app/g" -e "s|apps/$slug|apps/app|g"
+ bakeoff source --slug "$slug" --dir "${APP_DIR:-apps/$slug}" | sed -e "s/todo-$slug/todo-app/g" -e "s|apps/$slug|apps/app|g"
} > "$work/prompt.md"
replies=()
for i in 1 2 3; do
diff --git a/harness/cmd/bakeoff/main.go b/harness/cmd/bakeoff/main.go
index 1883503..398c420 100644
--- a/harness/cmd/bakeoff/main.go
+++ b/harness/cmd/bakeoff/main.go
@@ -36,6 +36,13 @@ func main() {
phase := fs.String("phase", "", "tokens: build|fix|security|review")
logs := fs.String("logs", "builders/logs", "tokens: log directory")
file := fs.String("file", "", "behaviour: Playwright JSON report")
+ var rec b.Record
+ fs.StringVar(&rec.Language, "language", "", "record: display name")
+ fs.StringVar(&rec.Status, "status", "", "record: LIVE or BLOCKED <reason>")
+ fs.StringVar(&rec.Version, "version", "", "record: deployed version")
+ fs.StringVar(&rec.Stack, "stack", "", "record: one-line stack summary")
+ fs.StringVar(&rec.Source, "source", "", "record: source URL")
+ recURL := fs.String("app-url", "", "record: live URL")
replies := fs.String("replies", "", "readability: comma-separated files, one scorer reply each")
fs.Parse(os.Args[2:])
if *slug == "" && cmd != "report" {
@@ -70,6 +77,11 @@ func main() {
err = renderReport(*file)
case "findings":
err = findings(*slug, *file)
+ case "record":
+ if rec.URL = *recURL; rec.URL == "" && strings.HasPrefix(rec.Status, "LIVE") {
+ rec.URL = *url
+ }
+ err = b.Update(*slug, func(r *b.AppResult) { b.ApplyRecord(r, rec) })
case "source":
err = source(*dir)
case "readability":
diff --git a/harness/harness_test.go b/harness/harness_test.go
index 0a4618c..c5a5953 100644
--- a/harness/harness_test.go
+++ b/harness/harness_test.go
@@ -254,3 +254,11 @@ func TestTokensSince(t *testing.T) {
t.Fatalf("got %+v", got)
}
}
+
+func TestApplyRecordKeepsUnsetFields(t *testing.T) {
+ r := AppResult{Language: "Go", Status: "LIVE", URL: "u", Version: "v1"}
+ ApplyRecord(&r, Record{Version: "v2"})
+ if r.Language != "Go" || r.Status != "LIVE" || r.URL != "u" || r.Version != "v2" {
+ t.Fatalf("got %+v", r)
+ }
+}
diff --git a/harness/results.go b/harness/results.go
index 4ecb228..8e810ad 100644
--- a/harness/results.go
+++ b/harness/results.go
@@ -72,3 +72,22 @@ func UpdateIn(dir, slug string, f func(*AppResult)) error {
// Update applies f to results/<slug>.json.
func Update(slug string, f func(*AppResult)) error { return UpdateIn(ResultsDir, slug, f) }
+
+// Record is a partial update of an app's identity fields; empty fields
+// leave the stored value alone.
+type Record struct{ Language, Status, URL, Version, Stack, Source string }
+
+// ApplyRecord copies the non-empty fields of rec into r.
+func ApplyRecord(r *AppResult, rec Record) {
+ set := func(dst *string, v string) {
+ if v != "" {
+ *dst = v
+ }
+ }
+ set(&r.Language, rec.Language)
+ set(&r.Status, rec.Status)
+ set(&r.URL, rec.URL)
+ set(&r.Version, rec.Version)
+ set(&r.Stack, rec.Stack)
+ set(&r.Source, rec.Source)
+}
diff --git a/results/go.json b/results/go.json
index 389aa63..d8ff348 100644
--- a/results/go.json
+++ b/results/go.json
@@ -1,9 +1,9 @@
{
"Slug": "go",
- "Language": "",
- "Status": "",
- "URL": "",
- "Version": "",
+ "Language": "Go",
+ "Status": "LIVE",
+ "URL": "https://todo-go.bfj.oncarlos.com",
+ "Version": "8a42c8d",
"Stack": "",
"Source": "",
"BehaviourJSOn": {
@@ -89,13 +89,13 @@
"CostUSD": 0
},
"Readability": {
- "Naming": 0,
- "Structure": 0,
- "Traceability": 0,
- "Errors": 0,
- "Comments": 0,
- "Overall": 0,
- "Notes": ""
+ "Naming": 4,
+ "Structure": 4,
+ "Traceability": 5,
+ "Errors": 4,
+ "Comments": 5,
+ "Overall": 8,
+ "Notes": "Strongest: comments consistently explain why (migration policy, single-writer pool, Origin check, focusout during swap), and the package docs state the security model up front. Request flow is short: route in New -\u003e itemAction -\u003e action -\u003e Store method -\u003e render \"list\". Error path is explicit via ErrNotFound and fail, with buffered rendering for clean 500s. Weakest: action/itemAction wrappers with inline closures and two differing callback signatures add a layer to step through; `one`, `w`/`r` fields and `bumpMarker` are terse or odd. loadStatic ignores fs.Sub and WalkDir errors, and the view struct mixes filter state, counts and Version. app.js swap/draft restoration is dense."
},
"ReviewFindings": [
"[spec] [apps/go/main.go:114](/home/paulca/amadan.net/carlos/todomvc-bakeoff/.worktrees/go/apps/go/main.go:114): `checkLoopback` accepts any loopback address, including `127.0.0.2` and `::1`. The brief explicitly requires binding only to `127.0.0.1`. These alternative `--listen` addresses pass validation and start a listener; restrict validation to the required address."