Skip to content

feat(agent): group semantically related files for multi-file review - #808

Open
lizhengfeng101 wants to merge 11 commits into
mainfrom
feat/semantic-file-grouping
Open

feat(agent): group semantically related files for multi-file review#808
lizhengfeng101 wants to merge 11 commits into
mainfrom
feat/semantic-file-grouping

Conversation

@lizhengfeng101

Copy link
Copy Markdown
Collaborator

Summary

  • Introduce semantic file grouping: related files (e.g., .go + _test.go, header + implementation) are reviewed together in a single LLM conversation for better cross-file analysis.
  • Add grouping.go with grouping logic and enforceMaxFilesPerGroup (max 10 files/group) + token budget enforcement.
  • Revamp plan/review prompts: replace JSON plan format with a structured MUST/SHOULD directive using [quick]/[deep] cost tags; remove MAY category to reduce wasted investigation rounds.
  • Update main_task_system.md to encourage "comment as you go" instead of batching comments until all items are investigated.
  • Preserve comments on failure: when all groups fail but CommentCollector already has comments, return them instead of discarding. Rounds-exhausted groups with existing comments are no longer marked as hard errors.
  • Adapt code_comment tool to accept a path field for multi-file group attribution.

Motivation

Per-file review misses cross-file inconsistencies (e.g., interface change without updating callers). Grouping related files in one conversation enables the model to catch these issues. The prompt and failure-handling changes address recall regression observed during evaluation (model over-investigating without producing comments, and produced comments being discarded on partial failures).

Test plan

  • make check passes (format, vet, license headers)
  • make test passes (unit tests including new grouping tests)
  • Run evaluation benchmark to confirm recall recovery

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 4 issue(s) in this PR.

  • ✅ Successfully posted inline: 4 comment(s)

Comment thread internal/agent/agent.go
Comment on lines 1195 to 1197
if a.args.CommentWorkerPool != nil {
a.args.CommentWorkerPool.AwaitKey(newPath)
a.args.CommentWorkerPool.AwaitKey(groupKey)
}

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.

[bug · critical]
CommentWorkerPool key mismatch: The AwaitKey call here uses groupKey (derived from fileGroupKey(g.Diffs)), but the LLM loop's executeToolCall (in internal/llmloop/loop.go:434) still submits work via pool.SubmitFor(newPath, ...) where newPath is the individual file path. Since groupKey for multi-file groups is a comma-separated sorted string of paths, AwaitKey(groupKey) will wait on a key that was never submitted, potentially blocking indefinitely or returning immediately (depending on the AwaitKey implementation for unknown keys). Either the submission key must also be groupKey, or a different awaiting strategy is needed.

Comment on lines +99 to +106
switch {
case d.IsNew:
status = "ADDED"
case d.IsDeleted:
status = "DELETED"
case d.IsRenamed:
status = "RENAMED"
}

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.

[maintainability · low]
The IsDeleted case in the switch statement is unreachable dead code. In agent.go:521-527, deleted files are filtered out before calling groupDiffs, so d.IsDeleted will never be true for any diff passed to this function. This dead code is misleading and adds unnecessary complexity. Either remove the case or add a comment explaining it's defensive programming for future-proofing.

Comment on lines +113 to +124
content = strings.TrimSpace(content)
// Strip markdown code fences if present
if strings.HasPrefix(content, "```") {
lines := strings.Split(content, "\n")
if len(lines) >= 2 {
lines = lines[1:]
}
if len(lines) > 0 && strings.HasPrefix(strings.TrimSpace(lines[len(lines)-1]), "```") {
lines = lines[:len(lines)-1]
}
content = strings.Join(lines, "\n")
}

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.

[maintainability · medium]
The fence-stripping logic in parseGroupingResponse duplicates functionality already available in stripMarkdownFences (util.go:34). The existing utility is more robust and handles language-tagged fences like ```json. Additionally, this manual implementation doesn't handle cases where the LLM adds explanatory text before/after the JSON block, which will cause parsing failures and trigger the per-file fallback unnecessarily. Consider using stripMarkdownFences or implementing JSON extraction logic similar to `parseFilterResponse` to improve robustness.

Suggestion:

Suggested change
content = strings.TrimSpace(content)
// Strip markdown code fences if present
if strings.HasPrefix(content, "```") {
lines := strings.Split(content, "\n")
if len(lines) >= 2 {
lines = lines[1:]
}
if len(lines) > 0 && strings.HasPrefix(strings.TrimSpace(lines[len(lines)-1]), "```") {
lines = lines[:len(lines)-1]
}
content = strings.Join(lines, "\n")
}
content = stripMarkdownFences(content)

Comment on lines +205 to +214
if total <= int64(tokenLimit) {
result = append(result, g)
} else {
for _, d := range g.Diffs {
result = append(result, FileGroup{
Label: g.Label + " (split: " + d.NewPath + ")",
Diffs: []model.Diff{d},
})
}
}

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.

[performance · medium]
When a group's total tokens exceed tokenLimit, every file in the group is split into its own single-file group, even if some files are small and could be safely combined. This can result in many unnecessary LLM calls. Consider a smarter splitting strategy that attempts to pack files into groups that fit within the budget, rather than immediately falling back to per-file dispatch for all files in the group.

@lizhengfeng101
lizhengfeng101 force-pushed the feat/semantic-file-grouping branch 2 times, most recently from 7924e26 to dfcf8aa Compare August 10, 2026 12:57
lizhengfeng101 and others added 11 commits August 10, 2026 21:56
Introduce LLM-based semantic file grouping so that related files (e.g.
implementation + test, i18n variants, interface + impl) are reviewed
together in a single LLM call instead of individually. This enables
cross-file consistency checks and reduces total LLM calls.

Key changes:
- Add grouping.go with LLM-based file grouping (GROUPING_TASK template)
  that clusters changed files by semantic relationship, with fallback
  to per-file dispatch on any error
- Refactor dispatchSubtasks to iterate over FileGroup instead of
  individual Diff, updating budget estimation, error handling, panic
  recovery, and session recording to work at group granularity
- Move the 'path' field from tool-level to per-comment level in
  code_comment tool schema, since one review call now covers multiple
  files — remove the forced path override in loop.go
- Rewrite plan phase output from JSON to a structured Review Directive
  with MUST/SHOULD/MAY severity tiers and concrete verification actions
- Update main_task prompts to accept {{diffs}} (multi-file XML) instead
  of {{diff}} + {{current_file_path}}, with cross-file review guidance
- Add enforceGroupTokenBudget and enforceMaxFilesPerGroup safety limits
- Treat partial completion (comments produced before round exhaustion)
  as partial success instead of hard error
ParseComments skips comments with empty path. After removing the
top-level args path override, these tests need path in each comment
object to match the new per-item path contract.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(llmloop): backfill comment thinking from the turn's reasoning or message

Expose ChatResponse.ReasoningContent and backfill per-comment thinking
with the current turn's reasoning content, falling back to the assistant
message for models that do not expose reasoning, so --format json output
carries thinking even when the model omits it.

* fix(llmloop): drop content fallback for comment thinking backfill

The turn's assistant message is usually a short user-facing preamble
rather than real reasoning, so backfill per-comment thinking only from
the model's native reasoning_content and leave it empty otherwise.

Add a full-wiring RunPerFile test for the reasoning backfill and a
regression test that fails if the content fallback returns. Sync the
thinking docs across en/zh/ja/ru.

* docs(llmloop): note that turn-level thinking is shared by design

Document in the main loop and at the code_comment backfill site that
the model emits reasoning once per turn, so every tool call and
comment in the same turn intentionally shares the same thinking.
The multi-file grouping change moved path from a top-level arg
to per-comment objects and removed the args["path"] = newPath
injection. This broke single-file RunPerFile calls where the model
does not emit path per comment.

Add ParseCommentsWithPath that applies newPath as a default when
comments lack an explicit path, restoring the previous behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The path field is already marked required in the tool JSON schema
with a clear description. Repeating it in the system prompt wastes
tokens without adding signal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The placeholder renders a plain one-per-line list, not a table.
The old name misleadingly suggests markdown table formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…<review_files>

The system prompt mentioned <review_files> but no such tag exists in
the rendered user message. The files are wrapped in <file path="...">
elements by buildConcatenatedDiffs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add <review_files> wrapper in the user prompt template around {{diffs}}
so the system prompt can reference it as the explicit review scope,
consistent with <other_changed_files> and <user_task> containers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Consolidate scope constraints into Strict Focus Rules; remove
  duplicated 'don't' items from Capabilities
- Remove redundant path instruction from user prompt (already in
  tool schema)
- Fix末尾 instruction to reference <review_files> instead of <file>
- Fix plan output format: each category numbers independently
- Replace // comments with plain text in user prompts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The grouping refactor deleted explanatory comments about budget
look-ahead semantics, why SetRunFailure is NOT used, and the
panic-isolation contract. These explain non-obvious WHY decisions
and are restored in condensed form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lizhengfeng101
lizhengfeng101 force-pushed the feat/semantic-file-grouping branch from dfcf8aa to df8e0d1 Compare August 10, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants