ace-review 0.51.4 → 0.53.5
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 +4 -5
- data/.ace-defaults/review/presets/code-fit.yml +3 -2
- data/.ace-defaults/review/presets/code-shine.yml +2 -2
- data/.ace-defaults/review/presets/code-valid.yml +3 -2
- data/.ace-defaults/review/presets/docs.yml +1 -1
- data/.ace-defaults/review/presets/spec.yml +1 -1
- data/CHANGELOG.md +79 -0
- data/README.md +16 -0
- data/handbook/skills/as-review-pr/SKILL.md +10 -1
- data/handbook/workflow-instructions/review/run.wf.md +3 -2
- data/lib/ace/review/molecules/feedback_synthesizer.rb +94 -49
- data/lib/ace/review/molecules/gh_comment_poster.rb +4 -3
- data/lib/ace/review/molecules/gh_comment_resolver.rb +6 -4
- data/lib/ace/review/molecules/gh_pr_comment_fetcher.rb +5 -4
- data/lib/ace/review/molecules/gh_pr_fetcher.rb +6 -4
- data/lib/ace/review/molecules/subject_extractor.rb +25 -1
- data/lib/ace/review/version.rb +1 -1
- data/lib/ace/review.rb +0 -1
- metadata +4 -5
- data/lib/ace/review/molecules/gh_cli_executor.rb +0 -124
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c50e105e74afc8802976a5c869debdd9514996f8c5c48c1ff0cc2266f1a2622f
|
|
4
|
+
data.tar.gz: 18512f481aa7315c446b7bfb93d7844dde492f1369b3a33cc34c3edb590bb1d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1869bc1bcbe622b59b88189ccbaf09192b183b44aac2a10d926b38befbb58f6e01a54601c218234d0e681532d590a4bf2d531d3ca5f027dda17957f19a046270
|
|
7
|
+
data.tar.gz: 68f5a18ae201735fb0577235d1a9bc53e703ea2e44c89e927b053e1048fa1bcfb2782facfed324a612b42cc8a07956fd96a99cc18599ebdd3538c86bcccb7105
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Configuration Options:
|
|
5
5
|
# ---------------------
|
|
6
6
|
# preset - Default preset when --preset not specified (default: "code-valid")
|
|
7
|
-
# model - Default LLM model for reviews (default: "
|
|
7
|
+
# model - Default LLM model for reviews (default: "role:review-default")
|
|
8
8
|
# output_format - Output format for reviews (default: "markdown")
|
|
9
9
|
# context - Default context preset (default: "project")
|
|
10
10
|
# max_concurrent_models - Max parallel LLM queries for multi-model execution (default: 3)
|
|
@@ -32,7 +32,7 @@ project_docs:
|
|
|
32
32
|
# Default settings applied to all reviews unless overridden
|
|
33
33
|
defaults:
|
|
34
34
|
preset: "code-valid" # Default preset when --preset not specified
|
|
35
|
-
model: "
|
|
35
|
+
model: "role:review-default"
|
|
36
36
|
output_format: "markdown"
|
|
37
37
|
bundle: "project"
|
|
38
38
|
max_concurrent_models: 3 # Max parallel LLM queries for multi-model execution
|
|
@@ -71,9 +71,8 @@ defaults:
|
|
|
71
71
|
# Feedback items are extracted from review reports for tracking and verification
|
|
72
72
|
feedback:
|
|
73
73
|
enabled: true # Enable feedback extraction (default: true)
|
|
74
|
-
synthesis_model:
|
|
75
|
-
|
|
76
|
-
- claude:sonnet
|
|
74
|
+
synthesis_model: role:review-synthesizer # Model for synthesizing feedback items
|
|
75
|
+
# fallback_models not needed — role:review-synthesizer has built-in multi-provider fallback
|
|
77
76
|
|
|
78
77
|
# Review presets - load from .ace/review/presets/*.yml
|
|
79
78
|
# Individual preset files provide better organization and shareability
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.53.5] - 2026-04-19
|
|
11
|
+
|
|
12
|
+
### Technical
|
|
13
|
+
- Stabilized retained multi-model and reviewers-format E2E fixtures by pinning them to installed direct review models instead of ambient mixed-provider availability.
|
|
14
|
+
|
|
15
|
+
## [0.53.4] - 2026-04-16
|
|
16
|
+
|
|
17
|
+
### Technical
|
|
18
|
+
- Hardened retained multi-model and docs-path review E2E runners to persist `.stdout`, `.stderr`, and `.exit` artifacts consistently across success, timeout, and provider-constrained paths.
|
|
19
|
+
|
|
20
|
+
## [0.53.3] - 2026-04-16
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Updated retained `TS-REVIEW-001` execution fixtures to use installed direct review models, require explicit diff subjects, and drive the single/multi-model flows through the public executable review path instead of stopping at session preparation.
|
|
24
|
+
|
|
25
|
+
## [0.53.2] - 2026-04-16
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Switched retained `TS-REVIEW-001` multi-model fixtures from the missing `review-claude` role to installed review roles so the scenario validates real multi-provider review execution instead of local CLI availability drift.
|
|
29
|
+
|
|
30
|
+
## [0.53.1] - 2026-04-16
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- Updated docs-path onboarding E2E verification to accept bounded timeout evidence when help discovery and review session artifacts prove the documented path is discoverable.
|
|
34
|
+
|
|
35
|
+
## [0.53.0] - 2026-04-15
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Rewrote retained `TS-REVIEW-001` E2E goals to enforce public docs/help command-path guidance and added
|
|
39
|
+
`TC-003-docs-path-onboarding` coverage.
|
|
40
|
+
- Tightened E2E verifier contracts so provider/model unavailability is treated as a failure path instead of a
|
|
41
|
+
PASS-equivalent outcome.
|
|
42
|
+
|
|
43
|
+
### Technical
|
|
44
|
+
- Updated scenario manifests and decision records to reflect three-goal execution (`X/3`) and the strict outcome
|
|
45
|
+
policy for onboarding-facing review workflows.
|
|
46
|
+
|
|
47
|
+
## [0.52.1] - 2026-04-13
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Completed the batch i05 migration follow-through for this package and aligned it with the restarted `fast` / `feat` / `e2e` verification model.
|
|
51
|
+
|
|
52
|
+
### Technical
|
|
53
|
+
- Included in the coordinated assignment-driven patch release for batch i05 package updates.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## [0.52.0] - 2026-04-12
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- Migrated package tests to the restarted `fast` / `feat` / `e2e` contract:
|
|
60
|
+
- moved deterministic package tests from legacy top-level folders into `test/fast/`
|
|
61
|
+
- moved former `test/integration/` deterministic coverage into `test/feat/`
|
|
62
|
+
- rewrote `TS-REVIEW-001` to retain only high-value execution workflows and added an E2E decision record
|
|
63
|
+
- Updated package docs to teach `ace-test ace-review`, `ace-test ace-review feat`, `ace-test ace-review all`, and `ace-test-e2e ace-review`.
|
|
64
|
+
- Expanded `as-review-pr` canonical skill metadata so public `review-pr` assign-step discovery is skill-owned rather than catalog-owned.
|
|
65
|
+
- Switched review GitHub CLI operations to use `Ace::Git::Molecules::GhCliExecutor` and removed the package-local `GhCliExecutor` implementation.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Added `diff:RANGE -- path` subject parsing so reviews can scope git diffs to specific files or directories with git-style path filters.
|
|
69
|
+
- Updated review GitHub workflows to correctly re-raise `Ace::Git` authentication/install errors after migrating to shared `GhCliExecutor`.
|
|
70
|
+
|
|
71
|
+
## [0.51.10] - 2026-04-07
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
- Switched review GitHub CLI integrations to shared `Ace::Git::Molecules::GhCliExecutor` to remove duplicate implementations and align error handling with other packages.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
- Preserved review-path authentication and install error behavior after the executor migration.
|
|
78
|
+
|
|
79
|
+
## [0.51.6] - 2026-03-31
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- Role-based review model, synthesis, and preset defaults.
|
|
83
|
+
|
|
84
|
+
## [0.51.5] - 2026-03-29
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- Bumped the `ace-bundle` runtime dependency constraint to `~> 0.41` to follow the new bundle minor release line.
|
|
88
|
+
|
|
10
89
|
## [0.51.4] - 2026-03-29
|
|
11
90
|
|
|
12
91
|
### Technical
|
data/README.md
CHANGED
|
@@ -38,5 +38,21 @@
|
|
|
38
38
|
|
|
39
39
|
**Audit review history through session artifacts** - keep saved review sessions under `.ace-local/` for traceability, comparison, and handoff across contributors.
|
|
40
40
|
|
|
41
|
+
## Testing
|
|
42
|
+
|
|
43
|
+
Run package deterministic checks with:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ace-test ace-review
|
|
47
|
+
ace-test ace-review feat
|
|
48
|
+
ace-test ace-review all
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run retained workflow scenarios with:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ace-test-e2e ace-review
|
|
55
|
+
```
|
|
56
|
+
|
|
41
57
|
---
|
|
42
58
|
[Getting Started](docs/getting-started.md) | [Usage Guide](docs/usage.md) | [Handbook - Skills, Agents, Templates](docs/handbook.md) | Part of [ACE](https://github.com/cs3b/ace)
|
|
@@ -22,10 +22,19 @@ integration:
|
|
|
22
22
|
- pi
|
|
23
23
|
providers: {}
|
|
24
24
|
assign:
|
|
25
|
-
source: wfi://review/pr
|
|
26
25
|
steps:
|
|
27
26
|
- name: review-pr
|
|
28
27
|
description: Review code changes for correctness, style, and best practices
|
|
28
|
+
prerequisites:
|
|
29
|
+
- name: create-pr
|
|
30
|
+
strength: required
|
|
31
|
+
reason: "Must have a PR to review"
|
|
32
|
+
produces: [review-feedback]
|
|
33
|
+
consumes: [pull-request]
|
|
34
|
+
when_to_skip:
|
|
35
|
+
- "No code changes since last review"
|
|
36
|
+
- "Changes are trivial (typo fix, config update)"
|
|
37
|
+
effort: medium
|
|
29
38
|
tags: [review, quality]
|
|
30
39
|
context:
|
|
31
40
|
default: fork
|
|
@@ -22,6 +22,7 @@ Review code using ace-review, verify feedback items, and create a plan for apply
|
|
|
22
22
|
|
|
23
23
|
- `staged`, `working` - keywords (no prefix needed)
|
|
24
24
|
- `diff:origin/main..HEAD` - git range (prefix required)
|
|
25
|
+
- `diff:origin/main...HEAD -- ace-test-runner-e2e` - git range filtered to paths
|
|
25
26
|
- `pr:123` - PR diff (prefix required)
|
|
26
27
|
- `files:lib/**/*.rb` - file pattern (prefix required)
|
|
27
28
|
- `task:145` - task context (prefix required)
|
|
@@ -41,7 +42,7 @@ ace-review --preset $1 --auto-execute
|
|
|
41
42
|
ace-review --subject "$2" --auto-execute
|
|
42
43
|
|
|
43
44
|
# File pattern review (NOTE: files: prefix is REQUIRED)
|
|
44
|
-
ace-review --preset spec --subject "files:.ace-
|
|
45
|
+
ace-review --preset spec --subject "files:.ace-task/**/*.md" --auto-execute
|
|
45
46
|
|
|
46
47
|
# Multiple subjects merge automatically
|
|
47
48
|
ace-review --subject pr:76 --subject files:CHANGELOG.md --auto-execute
|
|
@@ -259,4 +260,4 @@ The type prefix (`files:`, `pr:`, `diff:`, `task:`) is **required** for all subj
|
|
|
259
260
|
- [ ] Feedback items verified (Critical/High priority)
|
|
260
261
|
- [ ] False positives marked as invalid
|
|
261
262
|
- [ ] Confirmed items implemented with commits
|
|
262
|
-
- [ ] Items marked as resolved with commit references
|
|
263
|
+
- [ ] Items marked as resolved with commit references
|
|
@@ -26,6 +26,97 @@ module Ace
|
|
|
26
26
|
# result[:items] #=> [FeedbackItem, ...] (with reviewers arrays)
|
|
27
27
|
#
|
|
28
28
|
class FeedbackSynthesizer
|
|
29
|
+
# Cached system prompt and prompt-path lookups to avoid repeated
|
|
30
|
+
# shell/file reads during test and command-heavy runs.
|
|
31
|
+
FALLBACK_SYSTEM_PROMPT = <<~PROMPT.freeze
|
|
32
|
+
Synthesize feedback from code review reports into unique findings.
|
|
33
|
+
|
|
34
|
+
For each unique issue found:
|
|
35
|
+
1. Track which reviewers identified it (by their model names)
|
|
36
|
+
2. Merge file references from all sources
|
|
37
|
+
3. Use the most comprehensive description
|
|
38
|
+
4. Mark consensus=true if 3+ reviewers agree
|
|
39
|
+
|
|
40
|
+
Return valid JSON with this schema:
|
|
41
|
+
{
|
|
42
|
+
"findings": [
|
|
43
|
+
{
|
|
44
|
+
"title": "Short title (max 60 chars)",
|
|
45
|
+
"files": ["path/file.rb:10-20"],
|
|
46
|
+
"reviewers": ["gemini-2.5-flash", "claude-3.5-sonnet"],
|
|
47
|
+
"consensus": false,
|
|
48
|
+
"priority": "high|medium|low|critical",
|
|
49
|
+
"finding": "Description of the issue",
|
|
50
|
+
"context": "Why this matters"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
IMPORTANT:
|
|
56
|
+
- When only one report: extract all findings as-is with that reviewer
|
|
57
|
+
- When multiple reports: deduplicate findings that describe the same issue
|
|
58
|
+
- When multiple reviewers find the same issue, list ALL of them in reviewers array
|
|
59
|
+
- Merge file arrays from all sources for each finding
|
|
60
|
+
- Return ONLY the JSON, no markdown code fences
|
|
61
|
+
PROMPT
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
def system_prompt(prompt_name)
|
|
65
|
+
system_prompt_cache_mutex.synchronize do
|
|
66
|
+
system_prompt_cache.fetch(prompt_name) do
|
|
67
|
+
prompt_path = resolve_prompt_path_cached(prompt_name)
|
|
68
|
+
|
|
69
|
+
if prompt_path && File.exist?(prompt_path)
|
|
70
|
+
system_prompt_cache[prompt_name] = File.read(prompt_path)
|
|
71
|
+
else
|
|
72
|
+
system_prompt_cache[prompt_name] = FALLBACK_SYSTEM_PROMPT
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def resolve_prompt_path_cached(prompt_name)
|
|
79
|
+
prompt_path_cache[prompt_name] ||= begin
|
|
80
|
+
nav_result = begin
|
|
81
|
+
`ace-nav prompt://#{prompt_name} 2>/dev/null`.strip
|
|
82
|
+
rescue
|
|
83
|
+
""
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
return nav_result unless nav_result.empty?
|
|
87
|
+
|
|
88
|
+
File.join(__dir__, "../../../../handbook/prompts", prompt_name)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def clear_prompt_cache!
|
|
93
|
+
system_prompt_cache_mutex.synchronize do
|
|
94
|
+
@system_prompt_cache = {}
|
|
95
|
+
end
|
|
96
|
+
prompt_path_cache_mutex.synchronize do
|
|
97
|
+
@prompt_path_cache = {}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def system_prompt_cache
|
|
104
|
+
@system_prompt_cache ||= {}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def prompt_path_cache
|
|
108
|
+
@prompt_path_cache ||= {}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def system_prompt_cache_mutex
|
|
112
|
+
@system_prompt_cache_mutex ||= Mutex.new
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def prompt_path_cache_mutex
|
|
116
|
+
@prompt_path_cache_mutex ||= Mutex.new
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
29
120
|
# Maximum combined report size before truncation (characters)
|
|
30
121
|
MAX_COMBINED_SIZE = 200_000
|
|
31
122
|
|
|
@@ -138,51 +229,14 @@ module Ace
|
|
|
138
229
|
#
|
|
139
230
|
# @return [String] System prompt content
|
|
140
231
|
def load_system_prompt
|
|
141
|
-
|
|
142
|
-
prompt_path = resolve_prompt_path(prompt_name)
|
|
143
|
-
|
|
144
|
-
if File.exist?(prompt_path)
|
|
145
|
-
File.read(prompt_path)
|
|
146
|
-
else
|
|
147
|
-
fallback_synthesis_prompt
|
|
148
|
-
end
|
|
232
|
+
self.class.system_prompt("synthesize-feedback.system.md")
|
|
149
233
|
end
|
|
150
234
|
|
|
151
235
|
# Fallback synthesis prompt (used when prompt file not found)
|
|
152
236
|
#
|
|
153
237
|
# @return [String] Basic synthesis prompt
|
|
154
238
|
def fallback_synthesis_prompt
|
|
155
|
-
|
|
156
|
-
Synthesize feedback from code review reports into unique findings.
|
|
157
|
-
|
|
158
|
-
For each unique issue found:
|
|
159
|
-
1. Track which reviewers identified it (by their model names)
|
|
160
|
-
2. Merge file references from all sources
|
|
161
|
-
3. Use the most comprehensive description
|
|
162
|
-
4. Mark consensus=true if 3+ reviewers agree
|
|
163
|
-
|
|
164
|
-
Return valid JSON with this schema:
|
|
165
|
-
{
|
|
166
|
-
"findings": [
|
|
167
|
-
{
|
|
168
|
-
"title": "Short title (max 60 chars)",
|
|
169
|
-
"files": ["path/file.rb:10-20"],
|
|
170
|
-
"reviewers": ["gemini-2.5-flash", "claude-3.5-sonnet"],
|
|
171
|
-
"consensus": false,
|
|
172
|
-
"priority": "high|medium|low|critical",
|
|
173
|
-
"finding": "Description of the issue",
|
|
174
|
-
"context": "Why this matters"
|
|
175
|
-
}
|
|
176
|
-
]
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
IMPORTANT:
|
|
180
|
-
- When only one report: extract all findings as-is with that reviewer
|
|
181
|
-
- When multiple reports: deduplicate findings that describe the same issue
|
|
182
|
-
- When multiple reviewers find the same issue, list ALL of them in reviewers array
|
|
183
|
-
- Merge file arrays from all sources for each finding
|
|
184
|
-
- Return ONLY the JSON, no markdown code fences
|
|
185
|
-
PROMPT
|
|
239
|
+
FALLBACK_SYSTEM_PROMPT
|
|
186
240
|
end
|
|
187
241
|
|
|
188
242
|
# Build user prompt for report synthesis
|
|
@@ -522,16 +576,7 @@ module Ace
|
|
|
522
576
|
# @param prompt_name [String] Prompt filename
|
|
523
577
|
# @return [String] Resolved file path
|
|
524
578
|
def resolve_prompt_path(prompt_name)
|
|
525
|
-
|
|
526
|
-
nav_result = begin
|
|
527
|
-
`ace-nav prompt://#{prompt_name} 2>/dev/null`.strip
|
|
528
|
-
rescue
|
|
529
|
-
""
|
|
530
|
-
end
|
|
531
|
-
return nav_result unless nav_result.empty?
|
|
532
|
-
|
|
533
|
-
# Fallback to direct path
|
|
534
|
-
File.join(__dir__, "../../../../handbook/prompts", prompt_name)
|
|
579
|
+
self.class.resolve_prompt_path_cached(prompt_name)
|
|
535
580
|
end
|
|
536
581
|
|
|
537
582
|
# Get default synthesis model from config
|
|
@@ -53,7 +53,8 @@ module Ace
|
|
|
53
53
|
error: "Failed to post comment: #{result[:stderr]}"
|
|
54
54
|
}
|
|
55
55
|
end
|
|
56
|
-
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError
|
|
56
|
+
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
|
|
57
|
+
Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
|
|
57
58
|
raise
|
|
58
59
|
rescue => e
|
|
59
60
|
{
|
|
@@ -68,7 +69,7 @@ module Ace
|
|
|
68
69
|
# @return [Hash] Result with :success or :error
|
|
69
70
|
def self.check_pr_state(gh_format)
|
|
70
71
|
# Fetch PR metadata
|
|
71
|
-
result = Ace::
|
|
72
|
+
result = Ace::Git::Molecules::GhCliExecutor.execute(
|
|
72
73
|
"pr",
|
|
73
74
|
["view", gh_format, "--json", "state,number"]
|
|
74
75
|
)
|
|
@@ -169,7 +170,7 @@ module Ace
|
|
|
169
170
|
file.flush
|
|
170
171
|
|
|
171
172
|
# Post using gh pr comment
|
|
172
|
-
Ace::
|
|
173
|
+
Ace::Git::Molecules::GhCliExecutor.execute(
|
|
173
174
|
"pr",
|
|
174
175
|
["comment", gh_format, "--body-file", file.path]
|
|
175
176
|
)
|
|
@@ -34,7 +34,7 @@ module Ace
|
|
|
34
34
|
timeout = options[:timeout] || 30
|
|
35
35
|
|
|
36
36
|
# Post comment using gh CLI
|
|
37
|
-
result = Ace::
|
|
37
|
+
result = Ace::Git::Molecules::GhCliExecutor.execute(
|
|
38
38
|
"pr",
|
|
39
39
|
["comment", gh_format, "--body", body],
|
|
40
40
|
timeout: timeout
|
|
@@ -54,7 +54,8 @@ module Ace
|
|
|
54
54
|
error: "Failed to post reply: #{result[:stderr]}"
|
|
55
55
|
}
|
|
56
56
|
end
|
|
57
|
-
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError
|
|
57
|
+
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
|
|
58
|
+
Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
|
|
58
59
|
raise
|
|
59
60
|
rescue => e
|
|
60
61
|
{
|
|
@@ -91,7 +92,7 @@ module Ace
|
|
|
91
92
|
mutation = build_resolve_thread_mutation(thread_id)
|
|
92
93
|
|
|
93
94
|
# Execute via gh api graphql
|
|
94
|
-
result = Ace::
|
|
95
|
+
result = Ace::Git::Molecules::GhCliExecutor.execute(
|
|
95
96
|
"api",
|
|
96
97
|
["graphql", "-f", "query=#{mutation}"],
|
|
97
98
|
timeout: timeout
|
|
@@ -118,7 +119,8 @@ module Ace
|
|
|
118
119
|
error: "Failed to resolve thread: #{result[:stderr]}"
|
|
119
120
|
}
|
|
120
121
|
end
|
|
121
|
-
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError
|
|
122
|
+
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
|
|
123
|
+
Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
|
|
122
124
|
raise
|
|
123
125
|
rescue => e
|
|
124
126
|
{
|
|
@@ -82,7 +82,7 @@ module Ace
|
|
|
82
82
|
fields = "comments,reviews,number,title,author"
|
|
83
83
|
|
|
84
84
|
result = Ace::Review::Atoms::RetryWithBackoff.execute(options) do
|
|
85
|
-
Ace::
|
|
85
|
+
Ace::Git::Molecules::GhCliExecutor.execute("pr", ["view", gh_format, "--json", fields], timeout: timeout)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
if result[:success]
|
|
@@ -116,7 +116,8 @@ module Ace
|
|
|
116
116
|
success: false,
|
|
117
117
|
error: "Failed to parse PR comments: #{e.message}"
|
|
118
118
|
}
|
|
119
|
-
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError
|
|
119
|
+
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
|
|
120
|
+
Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
|
|
120
121
|
raise
|
|
121
122
|
rescue => e
|
|
122
123
|
{
|
|
@@ -250,7 +251,7 @@ module Ace
|
|
|
250
251
|
|
|
251
252
|
# Execute GraphQL query
|
|
252
253
|
result = Ace::Review::Atoms::RetryWithBackoff.execute(options) do
|
|
253
|
-
Ace::
|
|
254
|
+
Ace::Git::Molecules::GhCliExecutor.execute(
|
|
254
255
|
"api",
|
|
255
256
|
[
|
|
256
257
|
"graphql",
|
|
@@ -382,7 +383,7 @@ module Ace
|
|
|
382
383
|
# @return [String, nil] "owner/name" format or nil if not a GitHub repo
|
|
383
384
|
def self.discover_repo_from_remote(options = {})
|
|
384
385
|
timeout = options[:timeout] || 10
|
|
385
|
-
result = Ace::
|
|
386
|
+
result = Ace::Git::Molecules::GhCliExecutor.execute(
|
|
386
387
|
"repo",
|
|
387
388
|
["view", "--json", "owner,name"],
|
|
388
389
|
timeout: timeout
|
|
@@ -26,7 +26,7 @@ module Ace
|
|
|
26
26
|
|
|
27
27
|
# Fetch diff with retry logic
|
|
28
28
|
result = Ace::Review::Atoms::RetryWithBackoff.execute(options) do
|
|
29
|
-
Ace::
|
|
29
|
+
Ace::Git::Molecules::GhCliExecutor.execute("pr", ["diff", gh_format], timeout: timeout)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
if result[:success]
|
|
@@ -42,7 +42,8 @@ module Ace
|
|
|
42
42
|
rescue Ace::Review::Errors::DiffTooLargeError
|
|
43
43
|
# Fall back to local git diff when GitHub API rejects large diffs
|
|
44
44
|
fetch_local_diff_fallback(pr_identifier, options)
|
|
45
|
-
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError
|
|
45
|
+
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
|
|
46
|
+
Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
|
|
46
47
|
# Re-raise authentication and installation errors
|
|
47
48
|
raise
|
|
48
49
|
rescue => e
|
|
@@ -70,7 +71,7 @@ module Ace
|
|
|
70
71
|
fields = "number,state,isDraft,title,body,author,headRefName,baseRefName,url"
|
|
71
72
|
|
|
72
73
|
result = Ace::Review::Atoms::RetryWithBackoff.execute(options) do
|
|
73
|
-
Ace::
|
|
74
|
+
Ace::Git::Molecules::GhCliExecutor.execute("pr", ["view", gh_format, "--json", fields], timeout: timeout)
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
if result[:success]
|
|
@@ -89,7 +90,8 @@ module Ace
|
|
|
89
90
|
success: false,
|
|
90
91
|
error: "Failed to parse PR metadata: #{e.message}"
|
|
91
92
|
}
|
|
92
|
-
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError
|
|
93
|
+
rescue Ace::Review::Errors::GhCliNotInstalledError, Ace::Review::Errors::GhAuthenticationError,
|
|
94
|
+
Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
|
|
93
95
|
raise
|
|
94
96
|
rescue => e
|
|
95
97
|
{
|
|
@@ -177,7 +177,7 @@ module Ace
|
|
|
177
177
|
def parse_typed_subject(input)
|
|
178
178
|
case input
|
|
179
179
|
when /^diff:(.+)$/
|
|
180
|
-
{"bundle" =>
|
|
180
|
+
{"bundle" => parse_diff_subject(::Regexp.last_match(1))}
|
|
181
181
|
when /^diff:$/
|
|
182
182
|
raise ArgumentError, "Empty value for diff: subject. Usage: diff:RANGE (e.g., diff:HEAD~3...HEAD)"
|
|
183
183
|
when /^pr:(.+)$/
|
|
@@ -224,6 +224,30 @@ module Ace
|
|
|
224
224
|
end
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
+
def parse_diff_subject(value)
|
|
228
|
+
subject_value = value.to_s.strip
|
|
229
|
+
raise ArgumentError, "Empty value for diff: subject. Usage: diff:RANGE (e.g., diff:HEAD~3...HEAD)" if subject_value.empty?
|
|
230
|
+
|
|
231
|
+
if (match = subject_value.match(/\A(.+?)\s+--(?:\s+(.*))?\z/))
|
|
232
|
+
diff_range = match[1].to_s.strip
|
|
233
|
+
path_string = match[2].to_s
|
|
234
|
+
else
|
|
235
|
+
diff_range = subject_value
|
|
236
|
+
path_string = nil
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
raise ArgumentError, "Empty value for diff: subject. Usage: diff:RANGE (e.g., diff:HEAD~3...HEAD)" if diff_range.empty?
|
|
240
|
+
|
|
241
|
+
config = {"diffs" => [diff_range]}
|
|
242
|
+
return config if path_string.nil?
|
|
243
|
+
|
|
244
|
+
paths = path_string.split(/[,\s]+/).map(&:strip).reject(&:empty?).uniq
|
|
245
|
+
raise ArgumentError, "No valid paths specified after -- for diff: subject" if paths.empty?
|
|
246
|
+
|
|
247
|
+
config["paths"] = paths
|
|
248
|
+
config
|
|
249
|
+
end
|
|
250
|
+
|
|
227
251
|
# Default timeout for ace-task subprocess (in seconds)
|
|
228
252
|
# Can be overridden via options for environments with slow I/O
|
|
229
253
|
TASKFLOW_TIMEOUT = 10
|
data/lib/ace/review/version.rb
CHANGED
data/lib/ace/review.rb
CHANGED
|
@@ -37,7 +37,6 @@ require_relative "review/molecules/preset_manager"
|
|
|
37
37
|
require_relative "review/molecules/prompt_composer"
|
|
38
38
|
require_relative "review/molecules/nav_prompt_resolver"
|
|
39
39
|
require_relative "review/molecules/subject_extractor"
|
|
40
|
-
require_relative "review/molecules/gh_cli_executor"
|
|
41
40
|
require_relative "review/molecules/gh_pr_fetcher"
|
|
42
41
|
require_relative "review/molecules/gh_pr_comment_fetcher"
|
|
43
42
|
require_relative "review/molecules/gh_comment_poster"
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-review
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.53.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-04-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0.
|
|
74
|
+
version: '0.41'
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '0.
|
|
81
|
+
version: '0.41'
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: ace-git
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -300,7 +300,6 @@ files:
|
|
|
300
300
|
- lib/ace/review/molecules/feedback_file_reader.rb
|
|
301
301
|
- lib/ace/review/molecules/feedback_file_writer.rb
|
|
302
302
|
- lib/ace/review/molecules/feedback_synthesizer.rb
|
|
303
|
-
- lib/ace/review/molecules/gh_cli_executor.rb
|
|
304
303
|
- lib/ace/review/molecules/gh_comment_poster.rb
|
|
305
304
|
- lib/ace/review/molecules/gh_comment_resolver.rb
|
|
306
305
|
- lib/ace/review/molecules/gh_pr_comment_fetcher.rb
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open3"
|
|
4
|
-
require "timeout"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Review
|
|
8
|
-
module Molecules
|
|
9
|
-
# Safely execute gh CLI commands with error handling
|
|
10
|
-
class GhCliExecutor
|
|
11
|
-
# Default timeout for gh CLI operations
|
|
12
|
-
DEFAULT_GH_TIMEOUT = 30
|
|
13
|
-
|
|
14
|
-
# Execute a gh CLI command
|
|
15
|
-
#
|
|
16
|
-
# @param subcommand [String] The gh subcommand (e.g., "pr", "api")
|
|
17
|
-
# @param args [Array<String>] Arguments to pass to the subcommand
|
|
18
|
-
# @param options [Hash] Additional options
|
|
19
|
-
# @option options [Integer] :timeout Timeout in seconds (default: from config or 30)
|
|
20
|
-
# @return [Hash] Result with :success, :stdout, :stderr, :exit_code
|
|
21
|
-
def self.execute(subcommand, args = [], options = {})
|
|
22
|
-
check_installed
|
|
23
|
-
|
|
24
|
-
timeout_seconds = options[:timeout] ||
|
|
25
|
-
Ace::Review.get("defaults", "gh_timeout") ||
|
|
26
|
-
DEFAULT_GH_TIMEOUT
|
|
27
|
-
command = ["gh", subcommand] + args
|
|
28
|
-
|
|
29
|
-
run_command(command, timeout_seconds)
|
|
30
|
-
rescue Timeout::Error
|
|
31
|
-
raise Ace::Review::Errors::GhNetworkError, "gh command timed out after #{timeout_seconds} seconds"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Check if gh CLI is installed
|
|
35
|
-
#
|
|
36
|
-
# @return [Boolean] true if installed
|
|
37
|
-
# @raise [GhCliNotInstalledError] if not installed
|
|
38
|
-
def self.check_installed
|
|
39
|
-
result = execute_simple("--version")
|
|
40
|
-
result[:success]
|
|
41
|
-
rescue Ace::Review::Errors::GhCliNotInstalledError
|
|
42
|
-
raise
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Check if user is authenticated with GitHub
|
|
46
|
-
#
|
|
47
|
-
# @return [Hash] Auth status with :authenticated, :username
|
|
48
|
-
# @raise [GhAuthenticationError] if not authenticated
|
|
49
|
-
def self.check_authenticated
|
|
50
|
-
result = execute_simple("auth", ["status"])
|
|
51
|
-
|
|
52
|
-
if result[:success]
|
|
53
|
-
# Extract username from stderr (gh auth status outputs to stderr)
|
|
54
|
-
username = extract_username(result[:stderr])
|
|
55
|
-
{
|
|
56
|
-
authenticated: true,
|
|
57
|
-
username: username
|
|
58
|
-
}
|
|
59
|
-
else
|
|
60
|
-
raise Ace::Review::Errors::GhAuthenticationError
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Default timeout for simple operations
|
|
65
|
-
DEFAULT_SIMPLE_TIMEOUT = 10
|
|
66
|
-
|
|
67
|
-
# Execute a simple gh command without error checking
|
|
68
|
-
# Used internally to avoid infinite recursion in check_installed
|
|
69
|
-
#
|
|
70
|
-
# @param command [String] The gh subcommand
|
|
71
|
-
# @param args [Array<String>] Arguments
|
|
72
|
-
# @param timeout_seconds [Integer] Timeout in seconds (default: from config or 10)
|
|
73
|
-
# @return [Hash] Result hash
|
|
74
|
-
def self.execute_simple(command, args = [], timeout_seconds = nil)
|
|
75
|
-
timeout_seconds ||= Ace::Review.get("defaults", "gh_simple_timeout") || DEFAULT_SIMPLE_TIMEOUT
|
|
76
|
-
cmd = ["gh", command] + args
|
|
77
|
-
run_command(cmd, timeout_seconds)
|
|
78
|
-
rescue Timeout::Error
|
|
79
|
-
{
|
|
80
|
-
success: false,
|
|
81
|
-
stdout: "",
|
|
82
|
-
stderr: "Command timed out",
|
|
83
|
-
exit_code: 1
|
|
84
|
-
}
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Extract username from gh auth status output
|
|
88
|
-
#
|
|
89
|
-
# @param output [String] Output from gh auth status
|
|
90
|
-
# @return [String, nil] Username if found
|
|
91
|
-
def self.extract_username(output)
|
|
92
|
-
# gh auth status output format: "✓ Logged in to github.com as username ..."
|
|
93
|
-
match = output.match(/Logged in to .+ as (\S+)/)
|
|
94
|
-
match ? match[1] : nil
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Execute a command with timeout and error handling
|
|
98
|
-
# Private helper to reduce duplication between execute and execute_simple
|
|
99
|
-
#
|
|
100
|
-
# @param command [Array<String>] Full command array including "gh"
|
|
101
|
-
# @param timeout_seconds [Integer] Timeout in seconds
|
|
102
|
-
# @return [Hash] Result with :success, :stdout, :stderr, :exit_code
|
|
103
|
-
# @raise [GhCliNotInstalledError] if gh is not installed
|
|
104
|
-
# @raise [Timeout::Error] if command times out (caller should handle)
|
|
105
|
-
def self.run_command(command, timeout_seconds)
|
|
106
|
-
stdout_str, stderr_str, status = Timeout.timeout(timeout_seconds) do
|
|
107
|
-
Open3.capture3(*command)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
success: status.success?,
|
|
112
|
-
stdout: stdout_str,
|
|
113
|
-
stderr: stderr_str,
|
|
114
|
-
exit_code: status.exitstatus
|
|
115
|
-
}
|
|
116
|
-
rescue Errno::ENOENT
|
|
117
|
-
raise Ace::Review::Errors::GhCliNotInstalledError
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
private_class_method :execute_simple, :extract_username, :run_command
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|