Class: Aidp::Watch::PlanProcessor

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

Overview

Handles the aidp-plan label trigger by generating an implementation plan and posting it back to the originating GitHub issue.

Constant Summary collapse

TrackedSubIssuesRetirementError =
Class.new(StandardError)
DEFAULT_PLAN_LABEL =

Default label names

"aidp-plan"
DEFAULT_PROJECT_LABEL =
"aidp-project"
DEFAULT_NEEDS_INPUT_LABEL =
"aidp-needs-input"
DEFAULT_READY_LABEL =
"aidp-ready"
DEFAULT_BUILD_LABEL =
"aidp-build"
DEFAULT_BLOCKED_LABEL =
"aidp-blocked"
COMMENT_HEADER =
"## 🤖 AIDP Plan Proposal"

Constants included from MessageDisplay

MessageDisplay::COLOR_MAP, MessageDisplay::CRITICAL_TYPES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MessageDisplay

#display_message, included, #message_display_prompt, #quiet_mode?

Constructor Details

#initialize(repository_client:, state_store:, plan_generator:, label_config: {}, project_config: {}) ⇒ PlanProcessor

Returns a new instance of PlanProcessor.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/aidp/watch/plan_processor.rb', line 31

def initialize(repository_client:, state_store:, plan_generator:, label_config: {}, project_config: {})
  @repository_client = repository_client
  @state_store = state_store
  @plan_generator = plan_generator
  @project_config = project_config

  # Load label configuration with defaults
  @plan_label = label_config[:plan_trigger] || label_config["plan_trigger"] || DEFAULT_PLAN_LABEL
  @project_label = label_config[:project_trigger] || label_config["project_trigger"] || DEFAULT_PROJECT_LABEL
  @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
  @ready_label = label_config[:ready_to_build] || label_config["ready_to_build"] || DEFAULT_READY_LABEL
  @build_label = label_config[:build_trigger] || label_config["build_trigger"] || DEFAULT_BUILD_LABEL
  @blocked_label = label_config[:blocked_trigger] || label_config["blocked_trigger"] || DEFAULT_BLOCKED_LABEL
end

Instance Attribute Details

#blocked_labelObject (readonly)

Returns the value of attribute blocked_label.



29
30
31
# File 'lib/aidp/watch/plan_processor.rb', line 29

def blocked_label
  @blocked_label
end

#build_labelObject (readonly)

Returns the value of attribute build_label.



29
30
31
# File 'lib/aidp/watch/plan_processor.rb', line 29

def build_label
  @build_label
end

#needs_input_labelObject (readonly)

Returns the value of attribute needs_input_label.



29
30
31
# File 'lib/aidp/watch/plan_processor.rb', line 29

def needs_input_label
  @needs_input_label
end

#plan_labelObject (readonly)

Returns the value of attribute plan_label.



29
30
31
# File 'lib/aidp/watch/plan_processor.rb', line 29

def plan_label
  @plan_label
end

#project_labelObject (readonly)

Returns the value of attribute project_label.



29
30
31
# File 'lib/aidp/watch/plan_processor.rb', line 29

def project_label
  @project_label
end

#ready_labelObject (readonly)

Returns the value of attribute ready_label.



29
30
31
# File 'lib/aidp/watch/plan_processor.rb', line 29

def ready_label
  @ready_label
end

Instance Method Details

#process(issue, trigger_label: @plan_label) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
# File 'lib/aidp/watch/plan_processor.rb', line 46

def process(issue, trigger_label: @plan_label)
  number = issue[:number]
  existing_plan = @state_store.plan_data(number)
  project_mode = trigger_label.to_s.casecmp(@project_label).zero?

  if existing_plan
    display_message("🔄 Re-planning for issue ##{number} (iteration #{@state_store.plan_iteration_count(number) + 1})", type: :info)
  else
    prefix = project_mode ? "🗂️  Generating project plan" : "🧠 Generating plan"
    display_message("#{prefix} for issue ##{number} (#{issue[:title]})", type: :info)
  end

  plan_data = @plan_generator.generate(issue, hierarchical: project_mode)

  # If plan generation failed (all providers unavailable), silently skip
  unless plan_data
    Aidp.log_warn("plan_processor", "plan_generation_failed", issue: number, reason: "no plan data returned")
    display_message("⚠️  Unable to generate plan for issue ##{number} - all providers failed", type: :warn)
    return
  end

  # Fetch the user who added the most recent label
  label_actor = @repository_client.most_recent_label_actor(number)

  # If updating existing plan, archive the previous content
  archived_content = existing_plan ? archive_previous_plan(number, existing_plan) : nil

  comment_body = build_comment(
    issue: issue,
    plan: plan_data,
    project_mode: project_mode,
    label_actor: label_actor,
    archived_content: archived_content
  )
  comment_body_with_feedback = FeedbackCollector.append_feedback_prompt(comment_body)
  comment_id = nil

  if existing_plan && existing_plan["comment_id"]
    # Update existing comment
    @repository_client.update_comment(existing_plan["comment_id"], comment_body_with_feedback)
    comment_id = existing_plan["comment_id"]
    display_message("📝 Updated plan comment for issue ##{number}", type: :success)
  elsif existing_plan
    # Try to find existing comment by header
    existing_comment = @repository_client.find_comment(number, COMMENT_HEADER)
    if existing_comment
      @repository_client.update_comment(existing_comment[:id], comment_body_with_feedback)
      comment_id = existing_comment[:id]
      display_message("📝 Updated plan comment for issue ##{number}", type: :success)
    else
      # Fallback to posting new comment if we can't find the old one
      result = @repository_client.post_comment(number, comment_body_with_feedback)
      comment_id = result[:id] if result.is_a?(Hash)
      display_message("💬 Posted new plan comment for issue ##{number}", type: :success)
    end
  else
    # First time planning - post new comment
    result = @repository_client.post_comment(number, comment_body_with_feedback)
    comment_id = result[:id] if result.is_a?(Hash)
    display_message("💬 Posted plan comment for issue ##{number}", type: :success)
  end

  plan_data = plan_data.merge(comment_id: comment_id) if comment_id
  @state_store.record_plan(number, plan_data.merge(comment_body: comment_body, comment_hint: COMMENT_HEADER))

  project_setup = project_mode ? process_project_plan(issue, plan_data) : {status: :not_applicable}

  # Update labels: remove plan trigger, add appropriate status label
  update_labels_after_plan(
    number,
    plan_data,
    trigger_label: trigger_label,
    project_mode: project_mode,
    project_setup: project_setup
  )
end