ace-review 0.57.1 → 0.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/review/config.yml +2 -2
- data/.ace-defaults/review/presets/code-fit.yml +4 -3
- data/.ace-defaults/review/presets/code-shine.yml +1 -2
- data/.ace-defaults/review/presets/code-valid.yml +4 -3
- data/CHANGELOG.md +18 -0
- data/docs/feedback-workflow.md +4 -4
- data/docs/getting-started.md +1 -1
- data/docs/usage.md +3 -2
- data/lib/ace/review/atoms/pr_comment_formatter.rb +54 -9
- data/lib/ace/review/cli/commands/review.rb +16 -11
- data/lib/ace/review/errors.rb +0 -68
- data/lib/ace/review/models/review_options.rb +6 -3
- data/lib/ace/review/molecules/llm_executor.rb +5 -2
- data/lib/ace/review/molecules/pr_provider.rb +160 -0
- data/lib/ace/review/organisms/campaign_manager.rb +39 -14
- data/lib/ace/review/organisms/review_manager.rb +206 -55
- data/lib/ace/review/version.rb +1 -1
- data/lib/ace/review.rb +1 -4
- metadata +21 -11
- data/lib/ace/review/atoms/retry_with_backoff.rb +0 -75
- data/lib/ace/review/molecules/gh_comment_poster.rb +0 -207
- data/lib/ace/review/molecules/gh_comment_resolver.rb +0 -201
- data/lib/ace/review/molecules/gh_pr_comment_fetcher.rb +0 -410
- data/lib/ace/review/molecules/gh_pr_fetcher.rb +0 -307
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b74a181828465000d62fedc0062c118681144724a8beec940af5a335dade03b7
|
|
4
|
+
data.tar.gz: 700918d82fb6f27833a1b0f054301912d1a99dd5609b6140d93ff155a272f0b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3747d044dc834fe44d7977dfa120052709e5db45885a0c4fa39f7bbb60d4b819e6ea4c0f1f70c7189229b580f565b8936e6d5c1821ca04d4eb787a4cc84add61
|
|
7
|
+
data.tar.gz: 8ac3e30edd97356132095db4deca911ec62db8ba4c8ab0b92f7de8a11a8b6464cf5a9404180d8420d6eb18badb398ec158ee9e6f1b60a37f117d0dfbd4f6d6b6
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# context - Default context preset (default: "project")
|
|
10
10
|
# max_concurrent_models - Max parallel LLM queries for multi-model execution (default: 3)
|
|
11
11
|
# auto_execute - Auto-execute LLM query without confirmation (default: false)
|
|
12
|
-
#
|
|
12
|
+
# provider_timeout - Timeout for forge operations in seconds (default: 30)
|
|
13
13
|
# gh_simple_timeout - Timeout for simple gh commands like --version, auth status (default: 10)
|
|
14
14
|
# llm_timeout - Timeout for LLM queries in seconds (default: 900)
|
|
15
15
|
#
|
|
@@ -37,7 +37,7 @@ defaults:
|
|
|
37
37
|
bundle: "project"
|
|
38
38
|
max_concurrent_models: 3 # Max parallel LLM queries for multi-model execution
|
|
39
39
|
auto_execute: false # Set to true to auto-execute LLM query without confirmation
|
|
40
|
-
|
|
40
|
+
provider_timeout: 30 # Timeout for forge operations in seconds
|
|
41
41
|
gh_simple_timeout: 10 # Timeout for simple gh commands (--version, auth status)
|
|
42
42
|
llm_timeout: 900 # Timeout for LLM queries in seconds (15 minutes)
|
|
43
43
|
|
|
@@ -60,6 +60,7 @@ instructions:
|
|
|
60
60
|
bundle: "project"
|
|
61
61
|
|
|
62
62
|
models:
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
# One reviewer position with fallback chain (roles in ace-llm config):
|
|
64
|
+
# pi glm 5.3 -> codex sol -> agy flash 3.8; the round is good if even
|
|
65
|
+
# one returns. astra runs once per PR via role:review-astra.
|
|
66
|
+
- role:review-default
|
|
@@ -35,6 +35,7 @@ instructions:
|
|
|
35
35
|
bundle: "project"
|
|
36
36
|
|
|
37
37
|
models:
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
# One reviewer position with fallback chain (roles in ace-llm config):
|
|
39
|
+
# pi glm 5.3 -> codex sol -> agy flash 3.8; the round is good if even
|
|
40
|
+
# one returns. astra runs once per PR via role:review-astra.
|
|
41
|
+
- role:review-default
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.58.1] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Campaign finish scopes availability re-hashing to the newest round's evidence and the content-addressed finding snapshots; earlier rounds validate through journal-backed historical authority reads, so resolving findings no longer permanently blocks finish.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [0.58.0] - 2026-10-02
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Provider-neutral PR reviews and comment handling for GitHub, default Forgejo, and named Forgejo servers with exact-head evidence.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Review model queries honor the configured ace-llm fallback (role candidate
|
|
23
|
+
chains + `llm.fallback`) instead of forcing single-leg execution, so a
|
|
24
|
+
reviewer position survives a dead provider leg.
|
|
25
|
+
- Replaced `--gh-timeout` with `--provider-timeout`; removed the GitHub-only review fetcher, poster, and resolver paths.
|
|
26
|
+
- Preserve provider and repository identity across review sessions and comments; fail closed on moved heads and collection errors.
|
|
27
|
+
|
|
10
28
|
## [0.57.1] - 2026-10-02
|
|
11
29
|
|
|
12
30
|
### Fixed
|
data/docs/feedback-workflow.md
CHANGED
|
@@ -23,7 +23,7 @@ Key benefits:
|
|
|
23
23
|
|
|
24
24
|
1. Run a review with feedback extraction:
|
|
25
25
|
```bash
|
|
26
|
-
ace-review --preset code-
|
|
26
|
+
ace-review --preset code-valid --pr 123 --auto-execute
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
2. List feedback items:
|
|
@@ -245,7 +245,7 @@ feedback:
|
|
|
245
245
|
### Example Workflow Configuration
|
|
246
246
|
|
|
247
247
|
```yaml
|
|
248
|
-
# .ace/review/presets/code-
|
|
248
|
+
# .ace/review/presets/code-valid.yml
|
|
249
249
|
presets:
|
|
250
250
|
- code
|
|
251
251
|
|
|
@@ -416,10 +416,10 @@ Each review execution creates a new session:
|
|
|
416
416
|
|
|
417
417
|
```bash
|
|
418
418
|
# First review
|
|
419
|
-
ace-review --pr 189 --preset code-
|
|
419
|
+
ace-review --pr 189 --preset code-valid
|
|
420
420
|
|
|
421
421
|
# Second review (different PR or updated code)
|
|
422
|
-
ace-review --pr 190 --preset code-
|
|
422
|
+
ace-review --pr 190 --preset code-valid
|
|
423
423
|
|
|
424
424
|
# List all sessions
|
|
425
425
|
ls -la .ace-local/review/sessions/
|
data/docs/getting-started.md
CHANGED
|
@@ -38,7 +38,7 @@ This runs a code-focused review against your current branch diff and writes sess
|
|
|
38
38
|
Run:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
ace-review --pr 123 --preset code-
|
|
41
|
+
ace-review --pr 123 --preset code-valid --auto-execute
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Use `--post-comment` when you want to publish the review back to GitHub.
|
data/docs/usage.md
CHANGED
|
@@ -29,7 +29,7 @@ ace-review [OPTIONS]
|
|
|
29
29
|
|
|
30
30
|
| Option | Description |
|
|
31
31
|
|--------|-------------|
|
|
32
|
-
| `--preset` | Review preset
|
|
32
|
+
| `--preset` | Review preset from the shipped catalog, such as code-valid, code-fit, integration, or docs |
|
|
33
33
|
| `--pr` | Review GitHub PR (number, URL, or owner/repo#number) |
|
|
34
34
|
| `--subject` | Subject config (repeatable): `diff:range`, `diff:range -- path`, `files:glob`, `preset:name` |
|
|
35
35
|
| `--context` | Context config (preset name or YAML) |
|
|
@@ -43,7 +43,8 @@ ace-review [OPTIONS]
|
|
|
43
43
|
|--------|-------------|
|
|
44
44
|
| `--pr-comments` | Include PR comments as feedback source (default: true for --pr) |
|
|
45
45
|
| `--post-comment` | Post review as PR comment (requires --pr) |
|
|
46
|
-
| `--
|
|
46
|
+
| `--server` / `--default-server` | Select a configured forge server for a PR review |
|
|
47
|
+
| `--provider-timeout` | Timeout for forge operations in seconds (default: 30) |
|
|
47
48
|
|
|
48
49
|
### Prompt Composition
|
|
49
50
|
|
|
@@ -20,23 +20,41 @@ module Ace
|
|
|
20
20
|
|
|
21
21
|
# Format comments data into markdown report
|
|
22
22
|
#
|
|
23
|
-
# @param comments_data [Hash]
|
|
23
|
+
# @param comments_data [Hash] normalized provider comment data
|
|
24
24
|
# @return [String] Formatted markdown report
|
|
25
25
|
def self.format(comments_data)
|
|
26
26
|
return nil unless comments_data && comments_data[:success]
|
|
27
27
|
|
|
28
28
|
pr_number = comments_data[:pr_number]
|
|
29
29
|
pr_title = comments_data[:pr_title]
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
# Full provider evidence is preserved upstream; prompt feedback
|
|
31
|
+
# excludes bot posts, blank bodies, and our own session comments
|
|
32
|
+
# (they are not unresolved human feedback).
|
|
33
|
+
comments = (comments_data[:comments] || []).reject do |comment|
|
|
34
|
+
author = comment[:author].to_s
|
|
35
|
+
body = comment[:body].to_s
|
|
36
|
+
author.match?(/\b(bot|github-actions|ace-review)\b/i) ||
|
|
37
|
+
body.empty? ||
|
|
38
|
+
own_session_post?(body)
|
|
39
|
+
end
|
|
40
|
+
reviews = (comments_data[:reviews] || []).reject do |review|
|
|
41
|
+
review[:author].to_s.match?(/\b(bot|github-actions|ace-review)\b/i)
|
|
42
|
+
end
|
|
32
43
|
review_threads = comments_data[:review_threads] || []
|
|
33
44
|
|
|
45
|
+
# Apply the same bot filter to thread comments; threads left
|
|
46
|
+
# without human comments are omitted from feedback (filtered
|
|
47
|
+
# before the summary so bot-only threads never inflate counts).
|
|
48
|
+
review_threads = review_threads.map do |thread|
|
|
49
|
+
filtered = thread.merge(comments: Array(thread[:comments]).reject { |c| c[:author].to_s.match?(/\b(bot|github-actions|ace-review)\b/i) })
|
|
50
|
+
filtered.merge(comments: filtered[:comments])
|
|
51
|
+
end.reject { |thread| Array(thread[:comments]).empty? }
|
|
34
52
|
# Build report sections
|
|
35
53
|
frontmatter = build_frontmatter(comments_data)
|
|
36
54
|
summary = build_summary(comments, reviews, review_threads, pr_number, pr_title)
|
|
37
55
|
inline_section = build_inline_comments_section(review_threads)
|
|
38
56
|
unresolved_section = build_unresolved_section(comments, reviews)
|
|
39
|
-
resolved_section = build_resolved_section(reviews)
|
|
57
|
+
resolved_section = build_resolved_section(reviews, current_head: comments_data[:head_sha].to_s)
|
|
40
58
|
comments_table = build_comments_table(comments, reviews)
|
|
41
59
|
|
|
42
60
|
# Combine sections
|
|
@@ -76,6 +94,7 @@ module Ace
|
|
|
76
94
|
inline_thread_count = review_threads.size
|
|
77
95
|
reviewers = extract_reviewers(comments, reviews, review_threads)
|
|
78
96
|
unresolved_count = count_unresolved(comments, reviews, review_threads)
|
|
97
|
+
unknown_resolution = review_threads.count { |t| t[:is_resolved].nil? }
|
|
79
98
|
|
|
80
99
|
summary = "# Developer Feedback from PR ##{pr_number}\n\n"
|
|
81
100
|
summary += "> #{pr_title}\n\n" if pr_title && !pr_title.empty?
|
|
@@ -83,6 +102,7 @@ module Ace
|
|
|
83
102
|
summary += "- Total comments: #{total_comments}\n"
|
|
84
103
|
summary += "- Inline code comments: #{inline_thread_count}\n" if inline_thread_count > 0
|
|
85
104
|
summary += "- Unresolved items: #{unresolved_count}\n"
|
|
105
|
+
summary += "- Unknown-resolution threads: #{unknown_resolution}\n" if unknown_resolution > 0
|
|
86
106
|
summary += "- Reviewers: #{reviewers.map { |r| "@#{r}" }.join(", ")}\n" if reviewers.any?
|
|
87
107
|
summary
|
|
88
108
|
end
|
|
@@ -144,7 +164,11 @@ module Ace
|
|
|
144
164
|
line = thread[:line]
|
|
145
165
|
thread_id = thread[:id]
|
|
146
166
|
is_resolved = thread[:is_resolved]
|
|
147
|
-
status = is_resolved
|
|
167
|
+
status = case is_resolved
|
|
168
|
+
when true then "Resolved"
|
|
169
|
+
when false then "Unresolved"
|
|
170
|
+
else "Resolution unknown"
|
|
171
|
+
end
|
|
148
172
|
|
|
149
173
|
# Header with file:line, thread ID, and status
|
|
150
174
|
location = line ? "#{path}:#{line}" : path
|
|
@@ -167,13 +191,26 @@ module Ace
|
|
|
167
191
|
# Build resolved feedback section
|
|
168
192
|
#
|
|
169
193
|
# @param reviews [Array<Hash>] Code reviews
|
|
194
|
+
# @param current_head [String] Reviewed PR head (empty when unknown)
|
|
170
195
|
# @return [String, nil] Resolved section or nil if empty
|
|
171
|
-
def self.build_resolved_section(reviews)
|
|
196
|
+
def self.build_resolved_section(reviews, current_head: nil)
|
|
172
197
|
resolved = []
|
|
198
|
+
current = current_head.to_s
|
|
173
199
|
|
|
174
|
-
# Add approvals
|
|
200
|
+
# Add approvals. Approvals attach to a reviewed commit; only those
|
|
201
|
+
# matching the current head are current approvals, earlier-head
|
|
202
|
+
# approvals are labeled, and unknown-head approvals are called out
|
|
203
|
+
# as unknown instead of presenting as current.
|
|
175
204
|
reviews.select { |r| r[:state] == "APPROVED" }.each do |review|
|
|
176
|
-
|
|
205
|
+
review_head = review[:head_sha].to_s
|
|
206
|
+
label = if review_head.empty?
|
|
207
|
+
" (head unknown)"
|
|
208
|
+
elsif review_head == current
|
|
209
|
+
""
|
|
210
|
+
else
|
|
211
|
+
" (for earlier head #{review_head[0, 12]})"
|
|
212
|
+
end
|
|
213
|
+
resolved << "@#{review[:author]} approved changes#{label}"
|
|
177
214
|
end
|
|
178
215
|
|
|
179
216
|
return nil if resolved.empty?
|
|
@@ -186,6 +223,14 @@ module Ace
|
|
|
186
223
|
section
|
|
187
224
|
end
|
|
188
225
|
|
|
226
|
+
# An ACE post carries our review header AND a well-formed session
|
|
227
|
+
# marker as the trailing hidden line. A human reply quoting an
|
|
228
|
+
# earlier comment rarely matches both, so quotes stay visible.
|
|
229
|
+
def self.own_session_post?(body)
|
|
230
|
+
return false unless body.start_with?("## Code Review - ace-review")
|
|
231
|
+
body.match?(/<!--\s*ace-review-session:[a-zA-Z0-9._:-]+\s*-->\s*\z/)
|
|
232
|
+
end
|
|
233
|
+
|
|
189
234
|
# Build comments table for quick reference
|
|
190
235
|
#
|
|
191
236
|
# @param comments [Array<Hash>] Issue comments
|
|
@@ -256,7 +301,7 @@ module Ace
|
|
|
256
301
|
# @param review_threads [Array<Hash>] Inline review threads
|
|
257
302
|
# @return [Integer] Count of unresolved items
|
|
258
303
|
def self.count_unresolved(comments, reviews, review_threads = [])
|
|
259
|
-
unresolved_threads = review_threads.count { |t|
|
|
304
|
+
unresolved_threads = review_threads.count { |t| t[:is_resolved] == false }
|
|
260
305
|
actionable_comments = comments.count { |c| actionable_comment?(c[:body]) }
|
|
261
306
|
actionable_comments + reviews.count { |r| r[:state] == "CHANGES_REQUESTED" } + unresolved_threads
|
|
262
307
|
end
|
|
@@ -16,10 +16,11 @@ module Ace
|
|
|
16
16
|
Execute code review using presets or custom configuration
|
|
17
17
|
|
|
18
18
|
Presets provide pre-configured review types with focused prompts:
|
|
19
|
-
code
|
|
20
|
-
code-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
code-valid → Code correctness review
|
|
20
|
+
code-fit → Code design review
|
|
21
|
+
code-shine → Code quality review
|
|
22
|
+
integration → Integration review
|
|
23
|
+
spec → Specification review
|
|
23
24
|
docs → Documentation review
|
|
24
25
|
|
|
25
26
|
Configuration:
|
|
@@ -31,13 +32,13 @@ module Ace
|
|
|
31
32
|
DESC
|
|
32
33
|
|
|
33
34
|
example [
|
|
34
|
-
"--preset code-pr
|
|
35
|
-
"--preset
|
|
35
|
+
"--preset code-valid --pr 123 # PR code review",
|
|
36
|
+
"--preset code-fit --auto-execute # Run and apply fixes",
|
|
36
37
|
"--pr 123 # Review by PR number",
|
|
37
38
|
"--pr 123 --delta # Delta round since the most recent prior session's head",
|
|
38
39
|
"--pr 123 --delta <head> # Delta round since an explicit reviewed head",
|
|
39
|
-
"--preset code --subject diff:HEAD~3 --subject files:docs/**/*.md",
|
|
40
|
-
"--preset
|
|
40
|
+
"--preset code-valid --subject diff:HEAD~3 --subject files:docs/**/*.md",
|
|
41
|
+
"--preset code-fit --dry-run # Preview without executing"
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
# Review configuration options
|
|
@@ -58,10 +59,12 @@ module Ace
|
|
|
58
59
|
option :auto_execute, type: :boolean, default: nil, desc: "Execute LLM query automatically"
|
|
59
60
|
option :save_session, type: :boolean, desc: "Save session files (default: true)"
|
|
60
61
|
option :session_dir, type: :string, desc: "Custom session directory"
|
|
61
|
-
option :pr, type: :string, desc: "Review
|
|
62
|
+
option :pr, type: :string, desc: "Review PR (number, URL, or owner/repo#number)"
|
|
63
|
+
option :server, type: :string, desc: "Use the named configured forge server"
|
|
64
|
+
option :default_server, type: :boolean, desc: "Use the configured default forge server"
|
|
62
65
|
option :pr_comments, type: :boolean, desc: "Include PR comments as feedback source (default: true for --pr)"
|
|
63
66
|
option :post_comment, type: :boolean, desc: "Post review as PR comment (requires --pr)"
|
|
64
|
-
option :
|
|
67
|
+
option :provider_timeout, type: :integer, desc: "Timeout for forge operations in seconds (default: 30)"
|
|
65
68
|
option :evidence_session, type: :array, desc: "Include selected prior review sessions as context"
|
|
66
69
|
option :delta, type: :string, optional_value: true,
|
|
67
70
|
desc: "Delta round for --pr: review only changes since a reference head (explicit head, or `auto` to auto-resolve from the most recent prior session for this PR); omit for a full round"
|
|
@@ -106,7 +109,9 @@ module Ace
|
|
|
106
109
|
end
|
|
107
110
|
|
|
108
111
|
# Type-convert numeric options (ace-support-cli returns strings, Thor converted to integers)
|
|
109
|
-
|
|
112
|
+
if cli_options[:provider_timeout]
|
|
113
|
+
cli_options[:provider_timeout] = cli_options[:provider_timeout].to_i
|
|
114
|
+
end
|
|
110
115
|
|
|
111
116
|
# Build and store options for testing compatibility
|
|
112
117
|
@options = build_review_options(cli_options)
|
data/lib/ace/review/errors.rb
CHANGED
|
@@ -32,74 +32,6 @@ module Ace
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Raised when gh CLI is not installed
|
|
36
|
-
class GhCliNotInstalledError < Error
|
|
37
|
-
def initialize
|
|
38
|
-
message = "GitHub CLI (gh) is not installed.\n"
|
|
39
|
-
message += "Install with:\n"
|
|
40
|
-
message += " macOS: brew install gh\n"
|
|
41
|
-
message += " Linux: See https://cli.github.com/manual/installation\n"
|
|
42
|
-
message += " Windows: See https://cli.github.com/manual/installation"
|
|
43
|
-
|
|
44
|
-
super(message)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Raised when user is not authenticated with GitHub
|
|
49
|
-
class GhAuthenticationError < Error
|
|
50
|
-
def initialize
|
|
51
|
-
message = "GitHub authentication required.\n"
|
|
52
|
-
message += "Run 'gh auth login' to authenticate with GitHub.\n"
|
|
53
|
-
message += "Check status: gh auth status"
|
|
54
|
-
|
|
55
|
-
super(message)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Raised when a PR is not found
|
|
60
|
-
class PrNotFoundError < Error
|
|
61
|
-
attr_reader :pr_identifier
|
|
62
|
-
|
|
63
|
-
def initialize(pr_identifier, details = nil)
|
|
64
|
-
@pr_identifier = pr_identifier
|
|
65
|
-
message = "Pull request '#{pr_identifier}' not found."
|
|
66
|
-
message += "\n#{details}" if details
|
|
67
|
-
|
|
68
|
-
super(message)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Raised when PR diff exceeds GitHub's API size limit (300 files)
|
|
73
|
-
class DiffTooLargeError < Error
|
|
74
|
-
attr_reader :pr_identifier
|
|
75
|
-
|
|
76
|
-
def initialize(pr_identifier, details = nil)
|
|
77
|
-
@pr_identifier = pr_identifier
|
|
78
|
-
message = "Pull request '#{pr_identifier}' diff exceeds GitHub API size limit."
|
|
79
|
-
message += "\n#{details}" if details
|
|
80
|
-
|
|
81
|
-
super(message)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Raised when attempting to post to a closed/merged PR
|
|
86
|
-
class PrStateError < Error
|
|
87
|
-
attr_reader :pr_number, :state
|
|
88
|
-
|
|
89
|
-
def initialize(pr_number, state)
|
|
90
|
-
@pr_number = pr_number
|
|
91
|
-
@state = state
|
|
92
|
-
|
|
93
|
-
message = "Cannot post comment to PR ##{pr_number}.\n"
|
|
94
|
-
message += "PR is in '#{state}' state. Comments can only be posted to open PRs."
|
|
95
|
-
|
|
96
|
-
super(message)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Raised when gh CLI encounters a network error
|
|
101
|
-
class GhNetworkError < Error; end
|
|
102
|
-
|
|
103
35
|
# Raised when ace-task cannot find task
|
|
104
36
|
class TaskNotFoundError < Error
|
|
105
37
|
attr_reader :task_ref
|
|
@@ -9,7 +9,8 @@ module Ace
|
|
|
9
9
|
:prompt_base, :prompt_format, :prompt_focus, :add_focus,
|
|
10
10
|
:prompt_guidelines, :model, :models, :dry_run, :verbose,
|
|
11
11
|
:auto_execute, :save_session, :session_dir,
|
|
12
|
-
:pr, :post_comment, :pr_metadata, :
|
|
12
|
+
:pr, :post_comment, :pr_metadata, :provider_timeout,
|
|
13
|
+
:server, :default_server,
|
|
13
14
|
:pr_comments, :pr_comment_data,
|
|
14
15
|
:evidence_sessions, :campaign, :campaign_round, :campaign_scope,
|
|
15
16
|
:delta,
|
|
@@ -54,7 +55,9 @@ module Ace
|
|
|
54
55
|
@pr = hash[:pr]
|
|
55
56
|
@post_comment = hash[:post_comment] || false
|
|
56
57
|
@pr_metadata = hash[:pr_metadata]
|
|
57
|
-
@
|
|
58
|
+
@provider_timeout = hash[:provider_timeout]
|
|
59
|
+
@server = hash[:server]
|
|
60
|
+
@default_server = hash[:default_server] || false
|
|
58
61
|
|
|
59
62
|
# PR comment options
|
|
60
63
|
@pr_comments = hash[:pr_comments] # nil = use default, true/false = explicit
|
|
@@ -195,7 +198,7 @@ module Ace
|
|
|
195
198
|
end
|
|
196
199
|
end
|
|
197
200
|
|
|
198
|
-
@
|
|
201
|
+
@provider_timeout ||= config["provider_timeout"]
|
|
199
202
|
|
|
200
203
|
self
|
|
201
204
|
end
|
|
@@ -113,8 +113,11 @@ module Ace
|
|
|
113
113
|
output: output_file,
|
|
114
114
|
format: "text",
|
|
115
115
|
timeout: timeout || Ace::Review.get("defaults", "llm_timeout") || 300,
|
|
116
|
-
force: true
|
|
117
|
-
|
|
116
|
+
force: true
|
|
117
|
+
# Fallback is config-driven (llm.fallback + role candidate
|
|
118
|
+
# chains): the reviewer position tries pi glm 5.3, then codex
|
|
119
|
+
# sol, then agy flash 3.8 — the round is good if even one
|
|
120
|
+
# returns, and the session records the winner only.
|
|
118
121
|
)
|
|
119
122
|
|
|
120
123
|
unless result[:text].is_a?(String) && !result[:text].strip.empty?
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/git"
|
|
4
|
+
require "ace/git/github"
|
|
5
|
+
require "ace/git/forgejo"
|
|
6
|
+
require "ace/git/organisms/pull_request_lifecycle"
|
|
7
|
+
require "digest"
|
|
8
|
+
|
|
9
|
+
module Ace
|
|
10
|
+
module Review
|
|
11
|
+
module Molecules
|
|
12
|
+
# Review-facing projection of the shared, exact-head provider contract.
|
|
13
|
+
# Provider commands and response parsing remain in ace-git packages.
|
|
14
|
+
class PrProvider
|
|
15
|
+
def self.format_comment(review_text, preset: nil, model: nil, timestamp: nil)
|
|
16
|
+
header = "## Code Review - ace-review\n\n"
|
|
17
|
+
header += "**Preset**: #{preset}\n" if preset
|
|
18
|
+
header += "**Model**: #{model}\n" if model
|
|
19
|
+
header += "**Generated**: #{timestamp}\n" if timestamp
|
|
20
|
+
header += "\n" if preset || model || timestamp
|
|
21
|
+
content = review_text.to_s
|
|
22
|
+
content += "\n```\n" if content.scan(/^```/).count.odd?
|
|
23
|
+
header + <<~MARKDOWN
|
|
24
|
+
<details>
|
|
25
|
+
<summary><b>Full Review</b> (click to expand)</summary>
|
|
26
|
+
|
|
27
|
+
#{content}
|
|
28
|
+
|
|
29
|
+
</details>
|
|
30
|
+
MARKDOWN
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The forge server URL resolved by the lifecycle at construction;
|
|
34
|
+
# posting compares it with the reviewed repository before mutating.
|
|
35
|
+
def resolved_server_url(pr_identifier)
|
|
36
|
+
@lifecycle.resolved_server_url(pr_identifier)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(server_name: nil, use_default: false, timeout: nil, runner: nil, lifecycle: nil)
|
|
40
|
+
@lifecycle = lifecycle || Ace::Git::Organisms::PullRequestLifecycle.new(
|
|
41
|
+
server_name: server_name, use_default: use_default,
|
|
42
|
+
timeout: timeout, runner: runner
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fetch(pr_identifier, include_comments: true)
|
|
47
|
+
snapshot = @lifecycle.review_snapshot(pr_identifier, include_comments: include_comments)
|
|
48
|
+
metadata = metadata_for(snapshot)
|
|
49
|
+
{
|
|
50
|
+
success: true,
|
|
51
|
+
diff: snapshot.diff,
|
|
52
|
+
metadata: metadata,
|
|
53
|
+
comments: comments_for(snapshot),
|
|
54
|
+
checks: snapshot.checks,
|
|
55
|
+
snapshot: snapshot
|
|
56
|
+
}
|
|
57
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
58
|
+
{success: false, error: "#{e.class.name.split('::').last}: #{e.message}"}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Exact-head PR metadata only (no diff, comments, or checks); the
|
|
62
|
+
# delta resolver needs identity and base provenance, never the
|
|
63
|
+
# whole diff.
|
|
64
|
+
def fetch_metadata(pr_identifier)
|
|
65
|
+
snapshot = @lifecycle.review_metadata_snapshot(pr_identifier)
|
|
66
|
+
{success: true, metadata: metadata_for(snapshot)}
|
|
67
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
68
|
+
{success: false, error: "#{e.class.name.split('::').last}: #{e.message}"}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def post_comment(pr_identifier, expected_head:, content:, session_key:)
|
|
72
|
+
correlation = Digest::SHA256.hexdigest(session_key.to_s)
|
|
73
|
+
@lifecycle.post_review_comment(
|
|
74
|
+
pr_identifier, expected_head: expected_head,
|
|
75
|
+
body: content, correlation: correlation
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_thread(pr_identifier, expected_head:, thread_id:)
|
|
80
|
+
@lifecycle.resolve_review_thread(
|
|
81
|
+
pr_identifier, expected_head: expected_head, thread_id: thread_id
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def update_comment(pr_identifier, expected_head:, comment_id:, body:)
|
|
86
|
+
@lifecycle.update_review_comment(
|
|
87
|
+
pr_identifier, expected_head: expected_head,
|
|
88
|
+
comment_id: comment_id, body: body
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def file_at_ref(pr_identifier, path:, ref:)
|
|
93
|
+
@lifecycle.file_at_ref(pr_identifier, path: path, ref: ref)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def metadata_for(snapshot)
|
|
99
|
+
pr = snapshot.pull_request
|
|
100
|
+
{
|
|
101
|
+
"server_name" => pr.server_name,
|
|
102
|
+
"provider" => snapshot.provider.to_s,
|
|
103
|
+
"repository_url" => pr.base_repository_url,
|
|
104
|
+
"number" => pr.number,
|
|
105
|
+
"title" => pr.title,
|
|
106
|
+
"body" => pr.body,
|
|
107
|
+
"author" => {"login" => pr.author},
|
|
108
|
+
"state" => pr.state.to_s.upcase,
|
|
109
|
+
"isDraft" => pr.draft,
|
|
110
|
+
"baseRefName" => pr.base_ref,
|
|
111
|
+
"baseRefOid" => snapshot.base_sha,
|
|
112
|
+
"headRefName" => pr.head_ref,
|
|
113
|
+
"headRefOid" => pr.head_sha,
|
|
114
|
+
"head_repository_url" => pr.head_repository_url,
|
|
115
|
+
"url" => pr.url,
|
|
116
|
+
"changedFiles" => snapshot.files.length,
|
|
117
|
+
"files" => snapshot.files.map { |path| {"path" => path} },
|
|
118
|
+
"checks" => snapshot.checks.map { |check| check.to_h.transform_keys(&:to_s) }
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def comments_for(snapshot)
|
|
123
|
+
pr = snapshot.pull_request
|
|
124
|
+
comments = snapshot.review_evidence.comments
|
|
125
|
+
issue_comments = comments.reject(&:path).map do |comment|
|
|
126
|
+
{type: "issue_comment", id: comment.id, author: comment.author,
|
|
127
|
+
body: comment.body, url: comment.url}
|
|
128
|
+
end
|
|
129
|
+
inline = comments.select(&:path)
|
|
130
|
+
# Comments sharing a provider thread id belong to one thread; a
|
|
131
|
+
# nil thread id stays an honest per-comment entry.
|
|
132
|
+
threads = inline.select(&:thread_id).group_by(&:thread_id).map do |id, members|
|
|
133
|
+
first = members.first
|
|
134
|
+
{id: id, path: first.path, line: first.line, is_resolved: first.resolved,
|
|
135
|
+
comments: members.map { |comment|
|
|
136
|
+
{id: comment.id, author: comment.author, body: comment.body, url: comment.url}
|
|
137
|
+
}}
|
|
138
|
+
end
|
|
139
|
+
threads += inline.reject(&:thread_id).map do |comment|
|
|
140
|
+
{id: nil, path: comment.path, line: comment.line, is_resolved: comment.resolved,
|
|
141
|
+
comments: [{id: comment.id, author: comment.author, body: comment.body, url: comment.url}]}
|
|
142
|
+
end
|
|
143
|
+
reviews = snapshot.review_evidence.reviews.map do |review|
|
|
144
|
+
# Carry the reviewed commit so the formatter can tell
|
|
145
|
+
# current-head approvals from approvals of earlier heads.
|
|
146
|
+
{id: review.id, author: review.author, body: review.body,
|
|
147
|
+
state: review.state.to_s.upcase, url: review.url,
|
|
148
|
+
head_sha: review.head_sha}
|
|
149
|
+
end
|
|
150
|
+
{
|
|
151
|
+
success: true, comments: issue_comments, review_threads: threads,
|
|
152
|
+
reviews: reviews, pr_number: pr.number, pr_title: pr.title,
|
|
153
|
+
pr_author: pr.author, server_name: pr.server_name,
|
|
154
|
+
repository_url: pr.base_repository_url, head_sha: pr.head_sha
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|