ace-review 0.51.7 → 0.53.6
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/CHANGELOG.md +71 -0
- data/README.md +16 -0
- data/handbook/skills/as-review-pr/SKILL.md +10 -1
- data/handbook/workflow-instructions/review/run.wf.md +1 -1
- data/lib/ace/review/atoms/context_limit_resolver.rb +16 -143
- 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/llm_executor.rb +9 -2
- data/lib/ace/review/molecules/multi_model_executor.rb +10 -3
- data/lib/ace/review/molecules/strategies/adaptive_strategy.rb +1 -1
- data/lib/ace/review/version.rb +1 -1
- data/lib/ace/review.rb +0 -1
- metadata +2 -3
- 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: 647741c9f1b7814b5471a9a07c4f1693ac649f953d3f530bae67516a81cd7f0d
|
|
4
|
+
data.tar.gz: 520c59a0dda292b28d383215606fb7463d64d35f3b3ef0c024428243f2dc0870
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb3239ee728103949252327de0a7b97de54ea1747ca3a8c2c2fd1e930281091bcb57ce14de3ca44041bf2d95d14608883b753f4af2b85f2d4d019fd4829bfb32
|
|
7
|
+
data.tar.gz: 892722919f96459f188ddd278f8d42f0538f81297fd9c87729302ca2072f9c62901f58396a7a17a72bb6c677394eb1f341927c2b3a0d53b199450a8dd845d0a1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.53.6] - 2026-04-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Sized review prompts and adaptive review execution against the resolved concrete model target so role and alias-based reviews no longer rely on provider-wide context assumptions.
|
|
14
|
+
|
|
15
|
+
## [0.53.5] - 2026-04-19
|
|
16
|
+
|
|
17
|
+
### Technical
|
|
18
|
+
- Stabilized retained multi-model and reviewers-format E2E fixtures by pinning them to installed direct review models instead of ambient mixed-provider availability.
|
|
19
|
+
|
|
20
|
+
## [0.53.4] - 2026-04-16
|
|
21
|
+
|
|
22
|
+
### Technical
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
## [0.53.3] - 2026-04-16
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
30
|
+
## [0.53.2] - 2026-04-16
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- 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.
|
|
34
|
+
|
|
35
|
+
## [0.53.1] - 2026-04-16
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Updated docs-path onboarding E2E verification to accept bounded timeout evidence when help discovery and review session artifacts prove the documented path is discoverable.
|
|
39
|
+
|
|
40
|
+
## [0.53.0] - 2026-04-15
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Rewrote retained `TS-REVIEW-001` E2E goals to enforce public docs/help command-path guidance and added
|
|
44
|
+
`TC-003-docs-path-onboarding` coverage.
|
|
45
|
+
- Tightened E2E verifier contracts so provider/model unavailability is treated as a failure path instead of a
|
|
46
|
+
PASS-equivalent outcome.
|
|
47
|
+
|
|
48
|
+
### Technical
|
|
49
|
+
- Updated scenario manifests and decision records to reflect three-goal execution (`X/3`) and the strict outcome
|
|
50
|
+
policy for onboarding-facing review workflows.
|
|
51
|
+
|
|
52
|
+
## [0.52.1] - 2026-04-13
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
- Completed the batch i05 migration follow-through for this package and aligned it with the restarted `fast` / `feat` / `e2e` verification model.
|
|
56
|
+
|
|
57
|
+
### Technical
|
|
58
|
+
- Included in the coordinated assignment-driven patch release for batch i05 package updates.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## [0.52.0] - 2026-04-12
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- Migrated package tests to the restarted `fast` / `feat` / `e2e` contract:
|
|
65
|
+
- moved deterministic package tests from legacy top-level folders into `test/fast/`
|
|
66
|
+
- moved former `test/integration/` deterministic coverage into `test/feat/`
|
|
67
|
+
- rewrote `TS-REVIEW-001` to retain only high-value execution workflows and added an E2E decision record
|
|
68
|
+
- 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`.
|
|
69
|
+
- Expanded `as-review-pr` canonical skill metadata so public `review-pr` assign-step discovery is skill-owned rather than catalog-owned.
|
|
70
|
+
- Switched review GitHub CLI operations to use `Ace::Git::Molecules::GhCliExecutor` and removed the package-local `GhCliExecutor` implementation.
|
|
71
|
+
|
|
10
72
|
### Fixed
|
|
11
73
|
- Added `diff:RANGE -- path` subject parsing so reviews can scope git diffs to specific files or directories with git-style path filters.
|
|
74
|
+
- Updated review GitHub workflows to correctly re-raise `Ace::Git` authentication/install errors after migrating to shared `GhCliExecutor`.
|
|
75
|
+
|
|
76
|
+
## [0.51.10] - 2026-04-07
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
- Switched review GitHub CLI integrations to shared `Ace::Git::Molecules::GhCliExecutor` to remove duplicate implementations and align error handling with other packages.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- Preserved review-path authentication and install error behavior after the executor migration.
|
|
12
83
|
|
|
13
84
|
## [0.51.6] - 2026-03-31
|
|
14
85
|
|
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
|
|
@@ -42,7 +42,7 @@ ace-review --preset $1 --auto-execute
|
|
|
42
42
|
ace-review --subject "$2" --auto-execute
|
|
43
43
|
|
|
44
44
|
# File pattern review (NOTE: files: prefix is REQUIRED)
|
|
45
|
-
ace-review --preset spec --subject "files:.ace-
|
|
45
|
+
ace-review --preset spec --subject "files:.ace-task/**/*.md" --auto-execute
|
|
46
46
|
|
|
47
47
|
# Multiple subjects merge automatically
|
|
48
48
|
ace-review --subject pr:76 --subject files:CHANGELOG.md --auto-execute
|
|
@@ -1,161 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ace/llm"
|
|
4
|
+
|
|
3
5
|
module Ace
|
|
4
6
|
module Review
|
|
5
7
|
module Atoms
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# Maps model names to their context window sizes. Handles provider prefixes
|
|
9
|
-
# (google:, anthropic:, openai:) and uses pattern matching for model families.
|
|
10
|
-
#
|
|
11
|
-
# Resolution order:
|
|
12
|
-
# 1. ace-llm provider config (context_limit in providers/*.yml)
|
|
13
|
-
# 2. Hardcoded pattern matching (MODEL_LIMITS)
|
|
14
|
-
# 3. Conservative default for unknown models
|
|
15
|
-
#
|
|
16
|
-
# @example Basic usage
|
|
17
|
-
# ContextLimitResolver.resolve("google:gemini-2.5-pro")
|
|
18
|
-
# #=> 1_000_000
|
|
19
|
-
#
|
|
20
|
-
# @example Without provider prefix
|
|
21
|
-
# ContextLimitResolver.resolve("claude-3-sonnet")
|
|
22
|
-
# #=> 200_000
|
|
8
|
+
# Review-local wrapper around ace-llm's resolved model limit lookup.
|
|
23
9
|
module ContextLimitResolver
|
|
24
|
-
|
|
25
|
-
DEFAULT_LIMIT = 200_000
|
|
26
|
-
|
|
27
|
-
# Model patterns and their context limits (fallback when ace-llm config unavailable)
|
|
28
|
-
# Order matters - first match wins
|
|
29
|
-
# Patterns use regex for flexible matching
|
|
30
|
-
MODEL_LIMITS = [
|
|
31
|
-
# Gemini models
|
|
32
|
-
{pattern: /gemini-1\.5-pro/i, limit: 2_000_000},
|
|
33
|
-
{pattern: /gemini-1\.5-flash/i, limit: 1_000_000},
|
|
34
|
-
{pattern: /gemini-2\.5-pro/i, limit: 1_000_000},
|
|
35
|
-
{pattern: /gemini-2\.5-flash/i, limit: 1_000_000},
|
|
36
|
-
{pattern: /gemini-2\.0/i, limit: 1_000_000},
|
|
37
|
-
# Fallback for any other gemini model
|
|
38
|
-
{pattern: /gemini/i, limit: 1_000_000},
|
|
39
|
-
|
|
40
|
-
# Claude models (all variants: opus, sonnet, haiku)
|
|
41
|
-
{pattern: /claude.*opus/i, limit: 1_000_000},
|
|
42
|
-
{pattern: /claude.*sonnet/i, limit: 1_000_000},
|
|
43
|
-
{pattern: /claude.*haiku/i, limit: 1_000_000},
|
|
44
|
-
# Fallback for any other claude model
|
|
45
|
-
{pattern: /claude/i, limit: 1_000_000},
|
|
10
|
+
DEFAULT_LIMIT = Ace::LLM::Molecules::ModelLimitResolver::DEFAULT_CONTEXT_LIMIT
|
|
46
11
|
|
|
47
|
-
# OpenAI models
|
|
48
|
-
{pattern: /gpt-5\.\d/i, limit: 1_050_000},
|
|
49
|
-
{pattern: /o4-/i, limit: 1_050_000},
|
|
50
|
-
{pattern: /gpt-4o/i, limit: 128_000},
|
|
51
|
-
{pattern: /gpt-4-turbo/i, limit: 128_000},
|
|
52
|
-
{pattern: /gpt-4-32k/i, limit: 32_768},
|
|
53
|
-
{pattern: /gpt-4-\d+-preview/i, limit: 128_000}, # gpt-4-1106-preview, gpt-4-0125-preview
|
|
54
|
-
{pattern: /gpt-4-\d+$/i, limit: 8_192}, # legacy gpt-4-0613, etc.
|
|
55
|
-
{pattern: /gpt-4$/i, limit: 8_192}, # base gpt-4 model
|
|
56
|
-
{pattern: /o1-/i, limit: 200_000},
|
|
57
|
-
{pattern: /o3-/i, limit: 200_000}
|
|
58
|
-
].freeze
|
|
59
|
-
|
|
60
|
-
# Resolve context limit for a model
|
|
61
|
-
#
|
|
62
|
-
# Resolution order:
|
|
63
|
-
# 1. ace-llm provider config (if available and provider specified)
|
|
64
|
-
# 2. Hardcoded pattern matching
|
|
65
|
-
# 3. Default limit
|
|
66
|
-
#
|
|
67
|
-
# @param model_name [String, nil] Model identifier, optionally with provider prefix
|
|
68
|
-
# @return [Integer] Context limit in tokens
|
|
69
|
-
#
|
|
70
|
-
# @example With provider prefix
|
|
71
|
-
# ContextLimitResolver.resolve("google:gemini-2.5-pro")
|
|
72
|
-
# #=> 1_000_000
|
|
73
|
-
#
|
|
74
|
-
# @example Without provider prefix
|
|
75
|
-
# ContextLimitResolver.resolve("claude-3-opus")
|
|
76
|
-
# #=> 200_000
|
|
77
|
-
#
|
|
78
|
-
# @example Unknown model
|
|
79
|
-
# ContextLimitResolver.resolve("unknown-model")
|
|
80
|
-
# #=> 128_000
|
|
81
12
|
def self.resolve(model_name)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Try to get limit from ace-llm provider config first
|
|
85
|
-
limit = load_from_ace_llm(model_name)
|
|
86
|
-
return limit if limit
|
|
87
|
-
|
|
88
|
-
# Fall back to hardcoded pattern matching
|
|
89
|
-
normalized = strip_provider_prefix(model_name)
|
|
90
|
-
match = MODEL_LIMITS.find { |entry| normalized.match?(entry[:pattern]) }
|
|
91
|
-
match ? match[:limit] : DEFAULT_LIMIT
|
|
13
|
+
resolve_details(model_name).context_limit
|
|
92
14
|
end
|
|
93
15
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# @return [Integer] Default context limit
|
|
97
|
-
def self.default_limit
|
|
98
|
-
DEFAULT_LIMIT
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Load context limit from ace-llm provider configuration
|
|
102
|
-
#
|
|
103
|
-
# @param model_name [String] Model identifier with provider prefix
|
|
104
|
-
# @return [Integer, nil] Context limit from config, or nil if not found
|
|
105
|
-
def self.load_from_ace_llm(model_name)
|
|
106
|
-
# Extract provider prefix (e.g., "google" from "google:gemini-2.5-pro")
|
|
107
|
-
return nil unless model_name.include?(":")
|
|
108
|
-
|
|
109
|
-
provider = model_name.split(":").first
|
|
110
|
-
return nil if provider.nil? || provider.empty?
|
|
111
|
-
|
|
112
|
-
# Try to load provider config via ace-llm
|
|
113
|
-
config = load_provider_config(provider)
|
|
114
|
-
return nil unless config
|
|
115
|
-
|
|
116
|
-
# Get context_limit from provider config
|
|
117
|
-
limit = config["context_limit"]
|
|
118
|
-
limit.is_a?(Integer) ? limit : nil
|
|
16
|
+
def self.resolve_details(model_name)
|
|
17
|
+
Ace::LLM::Molecules::ModelLimitResolver.resolve(model_name)
|
|
119
18
|
rescue
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# @return [Hash, nil] Provider config hash or nil
|
|
128
|
-
def self.load_provider_config(provider)
|
|
129
|
-
# Try to use ace-llm's config loader if available
|
|
130
|
-
return nil unless defined?(Ace::LLM::Molecules::ConfigLoader)
|
|
131
|
-
|
|
132
|
-
resolver = Ace::Support::Config.create(
|
|
133
|
-
config_dir: ".ace",
|
|
134
|
-
defaults_dir: ".ace-defaults",
|
|
135
|
-
gem_path: Ace::LLM::Molecules::ConfigLoader.gem_root
|
|
19
|
+
Ace::LLM::Molecules::ModelLimitResolver::ResolveResult.new(
|
|
20
|
+
provider: nil,
|
|
21
|
+
model: nil,
|
|
22
|
+
context_limit: DEFAULT_LIMIT,
|
|
23
|
+
output_limit: nil,
|
|
24
|
+
source: :fallback,
|
|
25
|
+
original_target: model_name.to_s
|
|
136
26
|
)
|
|
137
|
-
|
|
138
|
-
config = resolver.resolve_namespace("llm", filename: "providers/#{provider}")
|
|
139
|
-
config.to_h
|
|
140
|
-
rescue
|
|
141
|
-
nil
|
|
142
27
|
end
|
|
143
|
-
private_class_method :load_provider_config
|
|
144
28
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# @param model_name [String] Model identifier
|
|
148
|
-
# @return [String] Model name without provider prefix
|
|
149
|
-
#
|
|
150
|
-
# @example
|
|
151
|
-
# strip_provider_prefix("google:gemini-2.5-pro")
|
|
152
|
-
# #=> "gemini-2.5-pro"
|
|
153
|
-
def self.strip_provider_prefix(model_name)
|
|
154
|
-
# Common provider prefixes
|
|
155
|
-
model_name.sub(/\A(google|anthropic|openai|codex|cli):/, "")
|
|
29
|
+
def self.default_limit
|
|
30
|
+
DEFAULT_LIMIT
|
|
156
31
|
end
|
|
157
|
-
|
|
158
|
-
private_class_method :strip_provider_prefix
|
|
159
32
|
end
|
|
160
33
|
end
|
|
161
34
|
end
|
|
@@ -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
|
{
|
|
@@ -67,12 +67,19 @@ module Ace
|
|
|
67
67
|
total_chars = (system_prompt&.length || 0) + (user_prompt&.length || 0)
|
|
68
68
|
estimated_tokens = total_chars / 4 # Rough estimate: 4 chars per token
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
limit_details = Ace::Review::Atoms::ContextLimitResolver.resolve_details(model)
|
|
71
|
+
context_limit = limit_details.context_limit
|
|
71
72
|
threshold = (context_limit * PROMPT_SIZE_WARNING_RATIO).to_i
|
|
72
73
|
return unless estimated_tokens > threshold
|
|
73
74
|
|
|
75
|
+
display_model = if limit_details.full_model && limit_details.full_model != model
|
|
76
|
+
"#{model} -> #{limit_details.full_model}"
|
|
77
|
+
else
|
|
78
|
+
model
|
|
79
|
+
end
|
|
80
|
+
|
|
74
81
|
warn "Warning: Prompt size (~#{estimated_tokens.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\\1,')} tokens) " \
|
|
75
|
-
"may exceed #{
|
|
82
|
+
"may exceed #{display_model} context limit (#{context_limit.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\\1,')} tokens)"
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
# Check if Ruby API is available
|
|
@@ -14,8 +14,8 @@ module Ace
|
|
|
14
14
|
# Default timeout for LLM queries (5 minutes)
|
|
15
15
|
DEFAULT_LLM_TIMEOUT = 300
|
|
16
16
|
|
|
17
|
-
# Warning threshold: 80% of
|
|
18
|
-
|
|
17
|
+
# Warning threshold: 80% of the smallest resolved context window in the batch
|
|
18
|
+
PROMPT_SIZE_WARNING_RATIO = LlmExecutor::PROMPT_SIZE_WARNING_RATIO
|
|
19
19
|
|
|
20
20
|
def initialize(max_concurrent: nil, llm_timeout: nil)
|
|
21
21
|
# Read from config, fallback to default of 3, clamp to minimum 1
|
|
@@ -266,7 +266,14 @@ module Ace
|
|
|
266
266
|
total_chars = (system_prompt&.length || 0) + (user_prompt&.length || 0)
|
|
267
267
|
estimated_tokens = total_chars / 4 # Rough estimate: 4 chars per token
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
resolved_limits = models.map do |model|
|
|
270
|
+
Atoms::ContextLimitResolver.resolve_details(model)
|
|
271
|
+
end
|
|
272
|
+
min_context_limit = resolved_limits.map(&:context_limit).compact.min
|
|
273
|
+
return if min_context_limit.nil?
|
|
274
|
+
|
|
275
|
+
threshold = (min_context_limit * PROMPT_SIZE_WARNING_RATIO).to_i
|
|
276
|
+
return unless estimated_tokens > threshold
|
|
270
277
|
|
|
271
278
|
model_list = models.join(", ")
|
|
272
279
|
warn "Warning: Prompt size (~#{estimated_tokens.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\\1,')} tokens) " \
|
|
@@ -70,7 +70,7 @@ module Ace
|
|
|
70
70
|
explicit_limit = context[:model_context_limit] || context["model_context_limit"]
|
|
71
71
|
|
|
72
72
|
# Resolve model context limit
|
|
73
|
-
model_limit = explicit_limit || Atoms::ContextLimitResolver.
|
|
73
|
+
model_limit = explicit_limit || Atoms::ContextLimitResolver.resolve_details(model).context_limit
|
|
74
74
|
|
|
75
75
|
# Select and delegate to appropriate strategy
|
|
76
76
|
selected = select_strategy(subject, model_limit, model)
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -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
|