Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions cmd/kai/review_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
41 changes: 41 additions & 0 deletions cmd/kai/review_commit_incomplete_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test defines and marshals its own local bundle struct instead of exercising the production struct in runReviewCommit, so it passes on the pre-change code and does not verify the fix; drive the JSON branch of the command (or extract the marshal into a testable helper) and assert on its output.

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)
}
}