Module: Brainiac::Plugins::Basecamp::CommentResponder
- Defined in:
- lib/brainiac/plugins/basecamp/comment_responder.rb
Overview
Handles inbound Basecamp comments on epic todolists/todos.
Routing:
1. If a bot account is @mentioned in the comment → dispatch that agent
2. If no mention → dispatch the last agent who responded on this epic
3. If no prior responder → dispatch the epic's default agent
The dispatched agent receives the comment content as a prompt with epic context, and posts its reply back via Client.add_comment.
Constant Summary collapse
- MENTION_TAG_OPEN =
"<bc-attachment"- MENTION_TAG_CLOSE =
"</bc-attachment>"
Class Method Summary collapse
-
.handle(payload, recording) ⇒ Array(Integer, String)
Process a comment_created webhook and dispatch the appropriate agent.
-
.resolve_person_ids(agent_names) ⇒ Hash<String, String>
Resolve Basecamp person IDs for known agent names using the basecamp CLI.
Class Method Details
.handle(payload, recording) ⇒ Array(Integer, String)
Process a comment_created webhook and dispatch the appropriate agent.
27 28 29 30 31 32 33 34 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 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/brainiac/plugins/basecamp/comment_responder.rb', line 27 def handle(payload, recording) content = recording["content"] || "" creator = payload["creator"] || {} creator_id = creator["id"]&.to_s parent = recording["parent"] || {} parent_type = parent["type"] parent_id = parent["id"] parent_title = parent["title"] || "" project_id = recording.dig("bucket", "id")&.to_s # Ignore comments posted by our own bot accounts (prevent loops) if Config.bot_account_for_person(creator_id) LOG.debug "[Basecamp:Comment] Ignoring comment from our own bot (person #{creator_id})" if defined?(LOG) return [200, { status: "ignored", reason: "self_comment" }.to_json] end # Determine if this comment is on an epic todolist or a todo within one epic = resolve_epic_for_comment(parent_type, parent_id, parent_title, project_id) unless epic LOG.debug "[Basecamp:Comment] Comment not on an epic recording — ignoring" if defined?(LOG) return [200, { status: "ignored", reason: "not_epic" }.to_json] end # Determine which agent to dispatch agent_name = resolve_target_agent(content, epic) LOG.info "[Basecamp:Comment] Dispatching #{agent_name} to respond to comment on '#{epic['title']}'" if defined?(LOG) # Strip HTML tags for a clean text prompt, preserve @mentions as names clean_content = strip_html_preserve_mentions(content) commenter_name = creator["name"] || "Someone" # Dispatch the agent in a background thread Thread.new do dispatch_comment_response( epic: epic, agent_name: agent_name, comment_text: clean_content, commenter_name: commenter_name, recording_id: parent_id, project_id: project_id ) rescue StandardError => e LOG.error "[Basecamp:Comment] Dispatch failed: #{e.}\n#{e.backtrace.first(3).join("\n")}" if defined?(LOG) end # Track last responding agent on the epic epic["last_responding_agent"] = agent_name epic["updated_at"] = Time.now.iso8601 Hooks.send(:save_epic_state, epic) [200, { status: "dispatched", agent: agent_name, epic_id: epic["id"] }.to_json] end |
.resolve_person_ids(agent_names) ⇒ Hash<String, String>
Resolve Basecamp person IDs for known agent names using the basecamp CLI. Used during setup to auto-map bot accounts.
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 |
# File 'lib/brainiac/plugins/basecamp/comment_responder.rb', line 87 def resolve_person_ids(agent_names) results = {} agent_names.each do |name| # Use jq to filter people by name output, status = Open3.capture2( "basecamp", "people", "list", "--jq", ".data[] | select(.name | ascii_downcase | contains(\"#{name.downcase}\")) | {id, name}" ) next unless status.success? # Parse each JSON line (could be multiple matches) output.each_line do |line| person = JSON.parse(line.strip) # Exact match preferred, otherwise first contains-match if person["name"]&.downcase == name.downcase results[name] = person["id"].to_s break elsif !results.key?(name) results[name] = person["id"].to_s end rescue JSON::ParserError next end end results end |