Class: Aidp::Watch::ChangeRequestProcessor

Inherits:
Object
  • Object
show all
Includes:
MessageDisplay
Defined in:
lib/aidp/watch/change_request_processor.rb

Overview

Handles the aidp-request-changes label trigger by analyzing PR comments and automatically implementing the requested changes.

Constant Summary collapse

DEFAULT_CHANGE_REQUEST_LABEL =

Default label names

"aidp-request-changes"
DEFAULT_NEEDS_INPUT_LABEL =
"aidp-needs-input"
COMMENT_HEADER =
"## 🤖 AIDP Change Request"
MAX_CLARIFICATION_ROUNDS =
3

Constants included from MessageDisplay

MessageDisplay::COLOR_MAP

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MessageDisplay

#display_message, included, #message_display_prompt

Constructor Details

#initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, change_request_config: {}, safety_config: {}, verbose: false) ⇒ ChangeRequestProcessor

Returns a new instance of ChangeRequestProcessor.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/aidp/watch/change_request_processor.rb', line 35

def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, change_request_config: {}, safety_config: {}, verbose: false)
  @repository_client = repository_client
  @state_store = state_store
  @state_extractor = GitHubStateExtractor.new(repository_client: repository_client)
  @provider_name = provider_name
  @project_dir = project_dir
  @verbose = verbose

  # Initialize verifier
  @verifier = ImplementationVerifier.new(
    repository_client: repository_client,
    project_dir: project_dir
  )

  # Load label configuration
  @change_request_label = label_config[:change_request_trigger] || label_config["change_request_trigger"] || DEFAULT_CHANGE_REQUEST_LABEL
  @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL

  # Load change request configuration
  @config = {
    enabled: true,
    allow_multi_file_edits: true,
    run_tests_before_push: true,
    commit_message_prefix: "aidp: pr-change",
    require_comment_reference: true,
    max_diff_size: 2000,
    allow_large_pr_worktree_bypass: true # Default to always using worktree for large PRs
  }.merge(symbolize_keys(change_request_config))

  # Load safety configuration
  @safety_config = safety_config
  @author_allowlist = Array(@safety_config[:author_allowlist] || @safety_config["author_allowlist"])
end

Instance Attribute Details

#change_request_labelObject (readonly)

Returns the value of attribute change_request_label.



33
34
35
# File 'lib/aidp/watch/change_request_processor.rb', line 33

def change_request_label
  @change_request_label
end

#needs_input_labelObject (readonly)

Returns the value of attribute needs_input_label.



33
34
35
# File 'lib/aidp/watch/change_request_processor.rb', line 33

def needs_input_label
  @needs_input_label
end

Instance Method Details

#process(pr) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/aidp/watch/change_request_processor.rb', line 69

def process(pr)
  number = pr[:number]

  unless @config[:enabled]
    display_message("â„šī¸  PR change requests are disabled in configuration. Skipping PR ##{number}.", type: :muted)
    return
  end

  # Check clarification round limit
  existing_data = @state_store.change_request_data(number)
  if existing_data && existing_data["clarification_count"].to_i >= MAX_CLARIFICATION_ROUNDS
    display_message("âš ī¸  Max clarification rounds (#{MAX_CLARIFICATION_ROUNDS}) reached for PR ##{number}. Skipping.", type: :warn)
    post_max_rounds_comment(pr)
    return
  end

  display_message("📝 Processing change request for PR ##{number} (#{pr[:title]})", type: :info)

  # Fetch PR details
  pr_data = @repository_client.fetch_pull_request(number)
  comments = @repository_client.fetch_pr_comments(number)

  # Filter comments from authorized users
  authorized_comments = filter_authorized_comments(comments, pr_data)

  if authorized_comments.empty?
    display_message("â„šī¸  No authorized comments found for PR ##{number}. Skipping.", type: :muted)
    return
  end

  # If max_diff_size is set, attempt to fetch and check diff
  # But bypass restriction for worktree-based workflows
  diff = @repository_client.fetch_pull_request_diff(number)
  diff_size = diff.lines.count

  # Check if we want to use the worktree bypass
  use_worktree_bypass = @config[:allow_large_pr_worktree_bypass] || @config[:allow_large_pr_worktree_bypass].nil?

  if diff_size > @config[:max_diff_size] && !use_worktree_bypass
    display_message("âš ī¸  PR ##{number} diff too large (#{diff_size} lines > #{@config[:max_diff_size]}). Skipping.", type: :warn)
    post_diff_too_large_comment(pr, diff_size)
    return
  end

  # Log the diff size for observability
  Aidp.log_debug("change_request_processor", "PR diff size", number: number, size: diff_size, max_allowed: @config[:max_diff_size], worktree_bypass: use_worktree_bypass)

  # Analyze change requests
  analysis_result = analyze_change_requests(pr_data: pr_data, comments: authorized_comments, diff: diff)

  if analysis_result[:needs_clarification]
    handle_clarification_needed(pr: pr_data, analysis: analysis_result)
  elsif analysis_result[:can_implement]
    implement_changes(pr: pr_data, analysis: analysis_result, diff: diff)
  else
    handle_cannot_implement(pr: pr_data, analysis: analysis_result)
  end
rescue => e
  display_message("❌ Change request processing failed: #{e.message}", type: :error)
  Aidp.log_error("change_request_processor", "Change request failed", pr: pr[:number], error: e.message, backtrace: e.backtrace&.first(10))

  # Record failure state internally but DON'T post error to GitHub
  # (per issue #280 - error messages should never appear on issues)
  @state_store.record_change_request(pr[:number], {
    status: "error",
    error: e.message,
    error_class: e.class.name,
    timestamp: Time.now.utc.iso8601
  })
end