fix(review): address trail + CI findings on scope context and allowlist · Entire

fix(review): address trail + CI findings on scope context and allowlist

7c7d292·

peyton-alt·1w ago·8 files·+75 added/-15 removed

Four review findings and one lint failure, all confirmed real:

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

320d86619050View transcript

Changes

8

50 unmodified lines

51
52
53
54
54
55
56
57

50 unmodified lines

"Read", "Grep", "Glob", "Task", "TodoWrite", "Skill",
"Bash(git diff:*)", "Bash(git log:*)", "Bash(git show:*)",
"Bash(git status:*)", "Bash(git blame:*)", "Bash(git rev-parse:*)",
"Bash(git merge-base:*)", "Bash(git ls-files:*)", "Bash(git branch:*)",
"Bash(git merge-base:*)", "Bash(git ls-files:*)",
"Bash(entire search:*)", "Bash(entire checkpoint explain:*)",
}

Mcmd/entire/cli/agent/claudecode/reviewer.go+1/-1

437 unmodified lines

438
439
440
441
441
442
443
444

437 unmodified lines

t.Errorf("allowlist missing %q; got %q", want, allowed)
    }
}
for _, banned := range []string{"Edit", "Write", "MultiEdit", "NotebookEdit", "Bash", "Bash(git:*)"} {
for _, banned := range []string{"Edit", "Write", "MultiEdit", "NotebookEdit", "Bash", "Bash(git:*)", "Bash(git branch:*)"} {
    if members[banned] {
        t.Errorf("allowlist must not grant write-capable or blanket tool %q; got %q", banned, allowed)
    }
}

Mcmd/entire/cli/agent/claudecode/reviewer_test.go+1/-1

135 unmodified lines

136
137
138
139
139
140
141
142
18 unmodified lines

161
162
163
164
164
165
166
167

135 unmodified lines

if err != nil {
        return "", settings.ReviewProfileConfig{}, err
    }
    profile.Task = guidedProfileTask(profileName, profile.Task, existing.Task, customTask)
    profile.Task = guidedProfileTask(profile.Task, existing.Task, customTask)
    if len(profile.Agents) > 1 {
        judge, err := promptForJudge(ctx, launchable, existing)
        if err != nil {
18 unmodified lines

// launchableInstalledAgentNames returns the installed agents that have a
// review-runner adapter, in the order they can be offered to the user.
func guidedProfileTask(profileName, generatedTask, existingTask, customTask string) string {
func guidedProfileTask(generatedTask, existingTask, customTask string) string {
    if customTask != "" {
        return customTask
    }

Mcmd/entire/cli/review/picker.go+2/-2

31 unmodified lines

32
33
34
35
35
36
37
38
38
39
40
41
41
42
43
44
28 unmodified lines

73
74
75
76
76
77
78
79

31 unmodified lines

existing  = "saved custom task"
        custom    = "new custom task"
    )
    if got := guidedProfileTask(DefaultProfileName, generated, existing, ""); got != existing {
    if got := guidedProfileTask(generated, existing, ""); got != existing {
        t.Fatalf("guidedProfileTask without new custom task = %q, want existing %q", got, existing)
    }
    if got := guidedProfileTask(DefaultProfileName, generated, existing, custom); got != custom {
    if got := guidedProfileTask(generated, existing, custom); got != custom {
        t.Fatalf("guidedProfileTask with new custom task = %q, want %q", got, custom)
    }
    if got := guidedProfileTask(DefaultProfileName, generated, "", ""); got != generated {
    if got := guidedProfileTask(generated, "", ""); got != generated {
        t.Fatalf("guidedProfileTask without existing task = %q, want generated %q", got, generated)
    }
}
28 unmodified lines

// skill-bearing workers kept receiving the maximal-audit brief forever.
func TestGuidedProfileTask_NoBuiltinFallbackPersisted(t *testing.T) {

t.Parallel()
    if got := guidedProfileTask(DefaultProfileName, "", "", ""); got != "" {
    if got := guidedProfileTask("", "", ""); got != "" {
        t.Fatalf("guidedProfileTask with nothing user-provided = %q, want empty", got)
    }
}

Mcmd/entire/cli/review/picker_internal_test.go+4/-4

130 unmodified lines

131
132
133
134
134
135
136
137
138
139
136
140
141
142
143
1 unmodified line

145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170

130 unmodified lines

switch {
    case sc.Diff != "":
        b.WriteString("\n\nDiff under review:\n```diff\n")
        // The fence must be longer than any backtick run inside the diff
        // (diffs touching markdown contain ``` lines), or the fence closes
        // early and diff content escapes into instruction position.
        fence := diffFence(sc.Diff)
        b.WriteString("\n\nDiff under review:\n" + fence + "diff\n")
        b.WriteString(strings.TrimRight(sc.Diff, "\n"))
        b.WriteString("\n```")
        b.WriteString("\n" + fence)
    case sc.DiffOmitted:
        b.WriteString("\n\nThe diff is too large to inline. Read it with `git diff " + baseRef + "...HEAD` (three-dot), plus `git status --porcelain` for uncommitted files.")
    }
1 unmodified line

return b.String()
}

// diffFence returns a backtick fence one longer than the longest backtick
// run in the diff (minimum the standard three).
func diffFence(diff string) string {
    longest, run := 0, 0
    for _, r := range diff {
        if r == '`' {
            run++
            if run > longest {
                longest = run
            }
            continue
        }
        run = 0
    }
    if longest < 3 {
        return "```"
    }
    return strings.Repeat("`", longest+1)
}

const reviewerOutputFormatInstructions = `Output format:
- Start with one verdict line: approve / approve with nits / request changes, plus a short reason.
- Then list actionable findings only. Each finding MUST be a separate top-level Markdown bullet starting with [high], [medium], or [low].

Mcmd/entire/cli/review/prompt.go+26/-2

315 unmodified lines

316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340

315 unmodified lines

t.Errorf("prompt missing non-whole-minute duration:\n%s", got)
    }
}

func TestComposeReviewPrompt_DiffWithBackticksKeepsFenceIntact(t *testing.T) {

t.Parallel()
    // A diff touching markdown can contain triple-backtick fences; a plain ```
    // wrapper would be closed early, corrupting the prompt structure and
    // letting diff content escape into instruction position.
    cfg := reviewtypes.RunConfig{
        Skills:       []string{"/x"},
        ScopeBaseRef: "main",
        ScopeContext: reviewtypes.ScopeContext{
            Files: []string{"M\tREADME.md"},
            Diff:  "diff --git a/README.md b/README.md\n+```go\n+code\n+```",
        },
    }
    got := ComposeReviewPrompt(cfg)
    if !strings.Contains(got, "````diff") {
        t.Errorf("diff containing ``` must be wrapped in a longer fence; got:\n%s", got)
    }
    if !strings.Contains(got, "\n````\n") && !strings.HasSuffix(got, "````") && !strings.Contains(got, "\n````") {
        t.Errorf("closing fence must match the longer opening fence; got:\n%s", got)
    }
}
}

Mcmd/entire/cli/review/prompt_test.go+22

`` 10 unmodified lines

11 12 13 14 15 16 17 245 unmodified lines

263 264 265 265 266 267 268 266 267 268 269 270 271 272 273 274 275 13 unmodified lines

289 290 291 288 292 293 294 295 296 297 298 299 300 301 302 303

10 unmodified lines

import ( "context" "fmt" "slices" "strconv" "strings"

245 unmodified lines

}

const ( scopeMaxCommits = 50 scopeMaxFiles = 200 scopeMaxUncommitted = 100 scopeDiffInlineLimit = 48 * 1024 scopeMaxCommits = 50 scopeMaxFiles = 200 scopeMaxUncommitted = 100 // scopeDiffInlineLimit stays well under Windows' ~32KiB CreateProcess // command-line cap (the prompt travels as one argv string and again via // ENTIRE_REVIEW_PROMPT), leaving headroom for the rest of the prompt. scopeDiffInlineLimit = 24 * 1024 )

// BuildScopeContext enumerates the review scope for prompt injection: the 13 unmodified lines

func buildScopeContextCapped(ctx context.Context, repoRoot, baseRef string, caps scopeContextCaps) (reviewtypes.ScopeContext, error) { var sc reviewtypes.ScopeContext

commitsOut, err := runGit(ctx, repoRoot, "log", "--reverse", "--format=%h %s", baseRef+"..HEAD") // Newest-first from git, cap (keeping the newest — the prompt declares // this list authoritative, and recent commits are the ones a review can // least afford to lose), then reverse for oldest-first reading order. commitsOut, err := runGit(ctx, repoRoot, "log", "--format=%h %s", baseRef+"..HEAD") if err != nil { return sc, fmt.Errorf("list scope commits: %w", err) } sc.Commits, sc.CommitsTruncated = capScopeLines(commitsOut, caps.maxCommits) slices.Reverse(sc.Commits)

filesOut, err := runGit(ctx, repoRoot, "diff", "--name-status", baseRef+"...HEAD") if err != nil {