From ad09c26141ef8a918eaeb1a362730f21a10ec0c9 Mon Sep 17 00:00:00 2001 From: kai CI Date: Tue, 8 Sep 2026 23:26:15 +0300 Subject: [PATCH] The incomplete-review bundle says so in a field, not just in prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #90 taught a review that ran out of road to report how far it got instead of vanishing. The prose it writes is honest. The bundle around it is not yet: `incomplete` stayed a local bool and never left this process. Downstream, kai-server picks the review's headline from the finding's counts — and an incomplete finding has no risks, no decisions and an unknown intent, because this command deliberately leaves match and readiness Unknown rather than guess a verdict. That is indistinguishable from a review that read everything and liked it, so kai-desktop#304 and kai-server#186 both opened with "Nothing jumped out" three lines above their own "This review did not finish" (2026-09-08). The renderer could not have known. The one process that did know was this one, and it dropped the fact on the floor. So it goes in the bundle. `omitempty` keeps a finished review's JSON byte-identical to what it was before this field existed — the flag is present only when it is true, so no complete review can be misread as an incomplete one, and an older server that does not know the field ignores it. The server-side companion consumes this and, until the pinned reviewer image carries this build, falls back to matching the prose. Co-Authored-By: Claude Opus 5 --- cmd/kai/review_commit.go | 14 ++++++-- cmd/kai/review_commit_incomplete_test.go | 41 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/cmd/kai/review_commit.go b/cmd/kai/review_commit.go index 4a9ea63..00d6cd8 100644 --- a/cmd/kai/review_commit.go +++ b/cmd/kai/review_commit.go @@ -446,11 +446,19 @@ func runReviewCommit(cmd *cobra.Command, args []string) error { if fast { depth = "fast" } + // incomplete rides along because the counts cannot carry it: a run + // that stopped before its conclusion emits no risks, no decisions and + // an unknown intent, which is arithmetically identical to a review + // that read everything and liked it. Without this flag the renderer + // has only the prose to go on, and it opened two timed-out reviews + // with "Nothing jumped out" directly above their own "This review did + // not finish" (kai-desktop#304, kai-server#186, 2026-09-08). out, err := json.MarshalIndent(struct { finding.Finding - Review string `json:"review,omitempty"` - Depth string `json:"depth,omitempty"` - }{f, prose, depth}, "", " ") + Review string `json:"review,omitempty"` + Depth string `json:"depth,omitempty"` + Incomplete bool `json:"incomplete,omitempty"` + }{f, prose, depth, incomplete}, "", " ") if err != nil { return fmt.Errorf("marshaling finding: %w", err) } diff --git a/cmd/kai/review_commit_incomplete_test.go b/cmd/kai/review_commit_incomplete_test.go index 6d07783..704f6f2 100644 --- a/cmd/kai/review_commit_incomplete_test.go +++ b/cmd/kai/review_commit_incomplete_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "strings" "testing" "time" @@ -89,3 +90,43 @@ func TestIncompleteReviewErrorSaysItDidNotFinish(t *testing.T) { t.Errorf("sentinel message should name the real problem, got %q", rcErrIncompleteReview) } } + +// The prose alone is not enough for the renderer downstream: kai-server picks +// the review's headline from the finding's COUNTS, and an incomplete bundle's +// counts are indistinguishable from a clean review's (no risks, no decisions, +// unknown intent). So the bundle has to carry the fact structurally. Without +// this field the server has only a literal sentence to match on, and two +// timed-out reviews shipped with "Nothing jumped out" as their opening line +// (kai-desktop#304, kai-server#186, 2026-09-08). +func TestIncompleteBundleCarriesTheFlag(t *testing.T) { + type bundle struct { + Review string `json:"review,omitempty"` + Depth string `json:"depth,omitempty"` + Incomplete bool `json:"incomplete,omitempty"` + } + + // The shape the JSON branch of runReviewCommit marshals when the run + // stopped: salvaged prose, and the flag that says it is not a review. + out, err := json.Marshal(bundle{Review: rcIncompleteProse(&rcIncomplete{ + FinishReason: string(message.FinishReasonTimeBudget), + Elapsed: 9*time.Minute + 59*time.Second, + Turns: 27, + }), Depth: "grounded", Incomplete: true}) + if err != nil { + t.Fatalf("marshaling bundle: %v", err) + } + if !strings.Contains(string(out), `"incomplete":true`) { + t.Errorf("bundle does not carry the incomplete flag:\n%s", out) + } + + // omitempty keeps a finished review's bundle byte-identical to what it + // was before this field existed — the flag appears only when it is true, + // so a complete review can never be read as an incomplete one. + done, err := json.Marshal(bundle{Review: "a real review", Depth: "grounded"}) + if err != nil { + t.Fatalf("marshaling complete bundle: %v", err) + } + if strings.Contains(string(done), "incomplete") { + t.Errorf("a complete review's bundle mentions incomplete:\n%s", done) + } +}