Skip to content

Commit b22a0db

Browse files
fix(orchestrator): the run headline could be raw planner JSON
pl.Summary is model-authored, and a model that was asked for JSON sometimes puts JSON there. Measured on a run that had otherwise gone perfectly: ✔ "assumptions": [ "The REST API will support basic CRUD operations for tasks with — 5/5 tasks done The counts and the gate verdict on that line were correct, so the run looked broken while being fine — the worst way for a summary to be wrong. The headline now refuses a serialized object and falls back to the board query, which is the request in the operator own words and always a truthful description of what the run was for. The last-resort string is the one firstSentence already uses, so the two paths cannot print a different "nothing to say".
1 parent 313aaf5 commit b22a0db

2 files changed

Lines changed: 116 additions & 1 deletion

File tree

‎pkg/orchestrator/headline_test.go‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package orchestrator
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/UnicoLab/slmcode/pkg/plan"
8+
)
9+
10+
// Measured on a finished run:
11+
//
12+
// ✔ "assumptions": [ "The REST API will support basic CRUD operations for tasks with — 5/5 tasks done
13+
//
14+
// The counts and the gate verdict on that line were correct, so the run looked
15+
// broken while being fine — the worst way for a summary to be wrong.
16+
func TestHeadlineRejectsRawPlannerJSON(t *testing.T) {
17+
board := &plan.Board{Query: "Build a Go REST backend in cmd/server/main.go"}
18+
for _, summary := range []string{
19+
`"assumptions": [ "The REST API will support basic CRUD operations for tasks with`,
20+
`{"summary":"build the thing","steps":[]}`,
21+
`[{"id":"T1"}]`,
22+
} {
23+
got := runHeadline(plan.Plan{Summary: summary}, board)
24+
if looksLikeRawJSON(got) {
25+
t.Errorf("headline is still JSON: %q", got)
26+
}
27+
if !strings.Contains(got, "Build a Go REST backend") {
28+
t.Errorf("did not fall back to the request: %q", got)
29+
}
30+
}
31+
}
32+
33+
// A real summary is used verbatim — the guard must not eat good output.
34+
func TestHeadlineKeepsARealSummary(t *testing.T) {
35+
board := &plan.Board{Query: "the raw request"}
36+
got := runHeadline(plan.Plan{Summary: "Add a task store and wire the server to it."}, board)
37+
if !strings.HasPrefix(got, "Add a task store") {
38+
t.Errorf("got %q, want the plan's own summary", got)
39+
}
40+
}
41+
42+
// With neither a usable summary nor a query, say something true rather than
43+
// printing an empty leading dash.
44+
func TestHeadlineFallsBackLast(t *testing.T) {
45+
if got := runHeadline(plan.Plan{}, &plan.Board{}); got != "Run complete" {
46+
t.Errorf("got %q, want a truthful fallback", got)
47+
}
48+
if got := runHeadline(plan.Plan{Summary: `{"a":1}`}, nil); got != "Run complete" {
49+
t.Errorf("nil board: got %q", got)
50+
}
51+
}
52+
53+
func TestLooksLikeRawJSON(t *testing.T) {
54+
for _, tc := range []struct {
55+
in string
56+
want bool
57+
}{
58+
{`{"summary":"x"}`, true},
59+
{`[{"id":"T1"}]`, true},
60+
{`"assumptions": [ "the api`, true},
61+
{"Add a task store and wire the server to it.", false},
62+
{"Fix the failing test in stats.go", false},
63+
{"", false},
64+
} {
65+
if got := looksLikeRawJSON(tc.in); got != tc.want {
66+
t.Errorf("looksLikeRawJSON(%q) = %v, want %v", tc.in, got, tc.want)
67+
}
68+
}
69+
}

‎pkg/orchestrator/orchestrator.go‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2543,7 +2543,7 @@ func summarizeWithRepairs(board *plan.Board, pl plan.Plan, repairs *repairLedger
25432543
}
25442544
}
25452545
out := fmt.Sprintf("%s — %d/%d tasks done, %d failed",
2546-
firstSentence(pl.Summary), done, total, board.FailedCount())
2546+
runHeadline(pl, board), done, total, board.FailedCount())
25472547
if line := repairs.line(); line != "" {
25482548
out += " · " + line
25492549
}
@@ -2835,3 +2835,49 @@ func truncate(s string, n int) string {
28352835
}
28362836
return s[:n] + "\n...[truncated]"
28372837
}
2838+
2839+
// runHeadline is the one clean sentence a finished run leads with.
2840+
//
2841+
// pl.Summary is model-authored, and a model that was asked for JSON sometimes
2842+
// puts JSON there. Measured: a run finished with
2843+
//
2844+
// ✔ "assumptions": [ "The REST API will support basic CRUD operations for tasks with — 5/5 tasks done
2845+
//
2846+
// which is the planner's raw object bleeding into the headline. The rest of the
2847+
// line — the counts, the gate verdict — was correct, so the run looked broken
2848+
// while being fine, which is the worst way for a summary to be wrong.
2849+
//
2850+
// Falls back to the board's own query, which is the request in the operator's
2851+
// words and always a truthful description of what the run was for.
2852+
func runHeadline(pl plan.Plan, board *plan.Board) string {
2853+
summary := strings.TrimSpace(pl.Summary)
2854+
if summary != "" && !looksLikeRawJSON(summary) {
2855+
if s := strings.TrimSpace(firstSentence(summary)); s != "" && !looksLikeRawJSON(s) {
2856+
return s
2857+
}
2858+
}
2859+
if board != nil {
2860+
if q := strings.TrimSpace(firstSentence(board.Query)); q != "" && !looksLikeRawJSON(q) {
2861+
return q
2862+
}
2863+
}
2864+
// Same words firstSentence falls back to, so the two paths cannot print a
2865+
// different "nothing to say".
2866+
return "Run complete"
2867+
}
2868+
2869+
// looksLikeRawJSON reports whether a string is a serialized object rather than
2870+
// prose. Deliberately shallow: it only has to catch a model echoing its own
2871+
// contract, not validate JSON.
2872+
func looksLikeRawJSON(s string) bool {
2873+
s = strings.TrimSpace(s)
2874+
if s == "" {
2875+
return false
2876+
}
2877+
if strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[") {
2878+
return true
2879+
}
2880+
// A quoted key followed by a colon — `"assumptions": [` — is the shape that
2881+
// survives a first-sentence cut of a pretty-printed object.
2882+
return strings.Contains(s, `":`) || strings.Contains(s, `" :`)
2883+
}

0 commit comments

Comments
 (0)