brainiac-fizzy 0.0.1
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 +7 -0
- data/README.md +111 -0
- data/lib/brainiac/plugins/fizzy/config.rb +104 -0
- data/lib/brainiac/plugins/fizzy/delegators.rb +106 -0
- data/lib/brainiac/plugins/fizzy/handlers/assignment.rb +128 -0
- data/lib/brainiac/plugins/fizzy/handlers/card_index.rb +389 -0
- data/lib/brainiac/plugins/fizzy/handlers/comments.rb +749 -0
- data/lib/brainiac/plugins/fizzy/handlers/dedup.rb +74 -0
- data/lib/brainiac/plugins/fizzy/handlers/deploy.rb +152 -0
- data/lib/brainiac/plugins/fizzy/handlers/deployments.rb +260 -0
- data/lib/brainiac/plugins/fizzy/helpers.rb +165 -0
- data/lib/brainiac/plugins/fizzy/hooks.rb +371 -0
- data/lib/brainiac/plugins/fizzy/planning.rb +73 -0
- data/lib/brainiac/plugins/fizzy/prompts.rb +119 -0
- data/lib/brainiac/plugins/fizzy/version.rb +9 -0
- data/lib/brainiac/plugins/fizzy.rb +212 -0
- data/lib/brainiac_fizzy.rb +4 -0
- metadata +128 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fizzy comment handler — routes incoming comments to the appropriate dispatch path.
|
|
4
|
+
#
|
|
5
|
+
# This is the main routing logic for Fizzy card comments:
|
|
6
|
+
# - Deploy shortcuts (dev01, dev02, etc.)
|
|
7
|
+
# - Cancel commands
|
|
8
|
+
# - Cross-agent mentions (@Galen on Kaylee's card)
|
|
9
|
+
# - Follow-up comments on existing worktrees
|
|
10
|
+
# - New mentions on untracked cards
|
|
11
|
+
|
|
12
|
+
# Context struct that accumulates state as a comment flows through the routing pipeline.
|
|
13
|
+
# Replaces long keyword-arg lists between sub-handlers.
|
|
14
|
+
CommentContext = Struct.new(
|
|
15
|
+
:eventable, :plain_text, :card_internal_id, :card_info,
|
|
16
|
+
:comment_id, :creator_name, :creator_is_agent,
|
|
17
|
+
:mentioned_agent, :agent_name, :is_cross_agent_mention,
|
|
18
|
+
:project_config, :project_key, :card_number, :worktree,
|
|
19
|
+
:model, :effort, :deploy_intent, :cli_provider_override,
|
|
20
|
+
:comment_vars, :card_tags, :worktree_override,
|
|
21
|
+
keyword_init: true
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def handle_comment(payload)
|
|
25
|
+
eventable = payload["eventable"] || {}
|
|
26
|
+
plain_text = eventable.dig("body", "plain_text") || ""
|
|
27
|
+
card_internal_id = eventable.dig("card", "id")
|
|
28
|
+
|
|
29
|
+
return handle_deploy_comment(eventable, plain_text.strip.downcase, card_internal_id) if plain_text.strip.match?(/\Adev\d+\z/i)
|
|
30
|
+
|
|
31
|
+
mentioned_agent = detect_mentioned_agent(plain_text)
|
|
32
|
+
gate_result = check_mention_gates(mentioned_agent, plain_text)
|
|
33
|
+
return gate_result if gate_result
|
|
34
|
+
|
|
35
|
+
creator_name, creator_is_agent, is_api_sourced = extract_creator_info(payload, eventable)
|
|
36
|
+
unless creator_is_agent || is_api_sourced
|
|
37
|
+
auth_result = authorize_human_comment(eventable, card_internal_id, creator_name, plain_text)
|
|
38
|
+
return auth_result if auth_result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
agent_result = validate_agent_comment(creator_is_agent, is_api_sourced, creator_name, mentioned_agent, card_internal_id)
|
|
42
|
+
return agent_result if agent_result
|
|
43
|
+
|
|
44
|
+
card_info = load_work_item_map[card_internal_id]
|
|
45
|
+
comment_id = eventable["id"]
|
|
46
|
+
|
|
47
|
+
return [200, { status: "ignored", reason: "not relevant" }.to_json] unless mentioned_agent || card_info
|
|
48
|
+
|
|
49
|
+
project_config, project_key = resolve_fizzy_project(card_info, card_internal_id, eventable)
|
|
50
|
+
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_config
|
|
51
|
+
|
|
52
|
+
tags = parse_inline_tags(plain_text)
|
|
53
|
+
|
|
54
|
+
agent_name, is_cross_agent_mention = resolve_comment_agent(
|
|
55
|
+
mentioned_agent: mentioned_agent, card_info: card_info, card_internal_id: card_internal_id,
|
|
56
|
+
eventable: eventable, project_config: project_config, creator_is_agent: creator_is_agent
|
|
57
|
+
)
|
|
58
|
+
return [200, { status: "ignored", reason: "no assigned agent" }.to_json] unless agent_name
|
|
59
|
+
|
|
60
|
+
cooldown_key = "card-#{card_info ? (card_info["number"] || card_internal_id) : card_internal_id}-#{agent_name.downcase}"
|
|
61
|
+
if on_comment_cooldown?(cooldown_key)
|
|
62
|
+
LOG.info "Skipping comment on #{cooldown_key} — within #{COMMENT_COOLDOWN}s cooldown"
|
|
63
|
+
return [200, { status: "ignored", reason: "comment cooldown" }.to_json]
|
|
64
|
+
end
|
|
65
|
+
touch_comment_cooldown(cooldown_key)
|
|
66
|
+
|
|
67
|
+
ctx = build_comment_context(
|
|
68
|
+
eventable: eventable, plain_text: plain_text, tags: tags, card_internal_id: card_internal_id,
|
|
69
|
+
card_info: card_info, comment_id: comment_id, creator_name: creator_name,
|
|
70
|
+
creator_is_agent: creator_is_agent, mentioned_agent: mentioned_agent,
|
|
71
|
+
agent_name: agent_name, is_cross_agent_mention: is_cross_agent_mention,
|
|
72
|
+
project_config: project_config, project_key: project_key
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# --- Route to appropriate sub-handler ---
|
|
76
|
+
if is_cross_agent_mention
|
|
77
|
+
handle_cross_agent_mention(ctx)
|
|
78
|
+
elsif card_info || ctx.worktree_override
|
|
79
|
+
handle_existing_card_comment(ctx)
|
|
80
|
+
else
|
|
81
|
+
handle_new_mention(ctx)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# --- Early-exit helpers ---
|
|
86
|
+
|
|
87
|
+
def build_comment_context(eventable:, plain_text:, tags:, card_internal_id:, card_info:, comment_id:, creator_name:,
|
|
88
|
+
creator_is_agent:, mentioned_agent:, agent_name:, is_cross_agent_mention:,
|
|
89
|
+
project_config:, project_key:)
|
|
90
|
+
deploy_intent = tags[:deploy_intent]
|
|
91
|
+
LOG.info "[Deploy] Detected [deploy#{":#{deploy_intent}" unless deploy_intent == :auto}] tag on card #{card_internal_id}" if deploy_intent
|
|
92
|
+
|
|
93
|
+
card_tags = eventable.dig("card", "tags") || []
|
|
94
|
+
clean_text = tags[:clean_text]
|
|
95
|
+
|
|
96
|
+
CommentContext.new(
|
|
97
|
+
eventable: eventable, plain_text: clean_text, card_internal_id: card_internal_id,
|
|
98
|
+
card_info: card_info, comment_id: comment_id, creator_name: creator_name,
|
|
99
|
+
creator_is_agent: creator_is_agent, mentioned_agent: mentioned_agent,
|
|
100
|
+
agent_name: agent_name, is_cross_agent_mention: is_cross_agent_mention,
|
|
101
|
+
project_config: project_config, project_key: project_key,
|
|
102
|
+
model: detect_model(project_config, text: plain_text),
|
|
103
|
+
effort: detect_effort(project_config, tags: card_tags, text: plain_text),
|
|
104
|
+
deploy_intent: deploy_intent,
|
|
105
|
+
cli_provider_override: detect_cli_provider(text: plain_text, tags: card_tags),
|
|
106
|
+
card_tags: card_tags,
|
|
107
|
+
worktree_override: resolve_worktree_override(tags, project_config),
|
|
108
|
+
comment_vars: {
|
|
109
|
+
"COMMENT_CREATOR" => creator_name || "Unknown",
|
|
110
|
+
"COMMENT_ID" => comment_id.to_s,
|
|
111
|
+
"COMMENT_BODY" => clean_text
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def check_mention_gates(mentioned_agent, plain_text)
|
|
117
|
+
mentioned_user_ids = detect_mentioned_user_ids(plain_text)
|
|
118
|
+
if mentioned_user_ids.any? { |id| human_mentioned?(id) }
|
|
119
|
+
LOG.info "[Fizzy] Human @mentioned in comment, skipping agent dispatch"
|
|
120
|
+
return [200, { status: "ignored", reason: "human mentioned" }.to_json]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if mentioned_agent && !local_agent_names.include?(mentioned_agent)
|
|
124
|
+
LOG.info "[Fizzy] Ignoring mention of non-local agent #{mentioned_agent}"
|
|
125
|
+
return [200, { status: "ignored", reason: "non-local agent mentioned" }.to_json]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def extract_creator_info(payload, eventable)
|
|
132
|
+
creator_name = eventable.dig("creator", "name")
|
|
133
|
+
creator_is_agent = comment_from_agent?(creator_name)
|
|
134
|
+
creator_is_agent ||= comment_from_agent?(payload.dig("creator", "name"))
|
|
135
|
+
|
|
136
|
+
source = eventable["source"] || payload["source"]
|
|
137
|
+
is_api_sourced = source && source != "web"
|
|
138
|
+
|
|
139
|
+
[creator_name, creator_is_agent, is_api_sourced]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def authorize_human_comment(eventable, card_internal_id, creator_name, plain_text)
|
|
143
|
+
creator_id = eventable.dig("creator", "id")
|
|
144
|
+
|
|
145
|
+
unless AUTHORIZED_USER_IDS.include?(creator_id)
|
|
146
|
+
notify_unauthorized("comment_created", creator_name, "card #{card_internal_id}")
|
|
147
|
+
return [200, { status: "ignored", reason: "unauthorized" }.to_json]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
record_human_comment(card_internal_id)
|
|
151
|
+
|
|
152
|
+
cancel_keywords = %w[cancel stop halt abort kill ❌]
|
|
153
|
+
return handle_cancel_command(eventable, card_internal_id) if cancel_keywords.include?(plain_text.strip.downcase)
|
|
154
|
+
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_agent_comment(creator_is_agent, is_api_sourced, creator_name, mentioned_agent, card_internal_id)
|
|
159
|
+
return nil unless creator_is_agent || is_api_sourced
|
|
160
|
+
|
|
161
|
+
card_info = load_work_item_map[card_internal_id]
|
|
162
|
+
card_assigned_agent = card_info&.dig("agent")
|
|
163
|
+
|
|
164
|
+
agent_is_assigned = card_assigned_agent && card_assigned_agent.downcase == (creator_name || "").downcase
|
|
165
|
+
agent_is_mentioned = mentioned_agent && mentioned_agent.downcase == (creator_name || "").downcase
|
|
166
|
+
|
|
167
|
+
unless agent_is_assigned || agent_is_mentioned
|
|
168
|
+
LOG.info "Blocking agent comment from #{creator_name} on card #{card_internal_id}: not assigned and not mentioned"
|
|
169
|
+
return [200, { status: "ignored", reason: "agent not assigned or mentioned" }.to_json]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Agent-to-agent loop prevention
|
|
173
|
+
if mentioned_agent && mentioned_agent.downcase != (creator_name || "").downcase
|
|
174
|
+
unless agent_dispatch_allowed?(card_internal_id)
|
|
175
|
+
LOG.info "Blocking agent-to-agent dispatch on card #{card_internal_id}: " \
|
|
176
|
+
"depth limit reached (#{creator_name} → @#{mentioned_agent})"
|
|
177
|
+
return [200, { status: "ignored", reason: "agent-to-agent depth limit" }.to_json]
|
|
178
|
+
end
|
|
179
|
+
LOG.info "Allowing agent-to-agent dispatch on card #{card_internal_id}: #{creator_name} → @#{mentioned_agent}"
|
|
180
|
+
elsif !mentioned_agent
|
|
181
|
+
LOG.info "Ignoring self-comment from #{creator_name} on card #{card_internal_id}"
|
|
182
|
+
return [200, { status: "ignored", reason: "self-comment" }.to_json]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def resolve_worktree_override(tags, project_config)
|
|
189
|
+
return nil unless tags[:worktree_override]
|
|
190
|
+
|
|
191
|
+
override_branch = tags[:worktree_override]
|
|
192
|
+
repo_path = project_config["repo_path"]
|
|
193
|
+
candidate = File.join(File.dirname(repo_path), "#{File.basename(repo_path)}--#{override_branch}")
|
|
194
|
+
|
|
195
|
+
if File.directory?(candidate)
|
|
196
|
+
LOG.info "Worktree override requested: #{override_branch} -> #{candidate}"
|
|
197
|
+
{ "branch" => override_branch, "worktree" => candidate }
|
|
198
|
+
else
|
|
199
|
+
LOG.warn "Worktree override branch '#{override_branch}' not found at #{candidate}, ignoring"
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# --- Comment sub-handlers ---
|
|
205
|
+
|
|
206
|
+
# --- Comment sub-handlers ---
|
|
207
|
+
|
|
208
|
+
def handle_cancel_command(eventable, card_internal_id)
|
|
209
|
+
killed = 0
|
|
210
|
+
card_number_for_cancel = load_work_item_map.dig(card_internal_id, "number")
|
|
211
|
+
prefixes = ["card-#{card_internal_id}"]
|
|
212
|
+
prefixes << "card-#{card_number_for_cancel}" if card_number_for_cancel
|
|
213
|
+
|
|
214
|
+
ACTIVE_SESSIONS_MUTEX.synchronize do
|
|
215
|
+
ACTIVE_SESSIONS.keys.select { |k| prefixes.any? { |p| k == p || k.start_with?("#{p}-") } }.each do |key|
|
|
216
|
+
info = ACTIVE_SESSIONS[key]
|
|
217
|
+
next unless info
|
|
218
|
+
|
|
219
|
+
begin
|
|
220
|
+
Process.kill("KILL", info[:pid])
|
|
221
|
+
LOG.info "[Fizzy] Cancelled session #{key} (PID: #{info[:pid]})"
|
|
222
|
+
rescue Errno::ESRCH, Errno::EPERM => e
|
|
223
|
+
LOG.warn "[Fizzy] Could not kill #{key}: #{e.message}"
|
|
224
|
+
end
|
|
225
|
+
archive_session(key, info)
|
|
226
|
+
ACTIVE_SESSIONS.delete(key)
|
|
227
|
+
killed += 1
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
comment_id_for_cancel = eventable["id"]
|
|
232
|
+
card_info_for_cancel = load_work_item_map[card_internal_id]
|
|
233
|
+
if card_info_for_cancel && card_number_for_cancel && comment_id_for_cancel
|
|
234
|
+
repo = (card_info_for_cancel["project"] && PROJECTS.dig(card_info_for_cancel["project"], "repo_path")) ||
|
|
235
|
+
DEFAULT_PROJECT["repo_path"]
|
|
236
|
+
Thread.new do
|
|
237
|
+
run_cmd("fizzy", "reaction", "create", "--card", card_number_for_cancel.to_s,
|
|
238
|
+
"--comment", comment_id_for_cancel.to_s, "--content", "🛑",
|
|
239
|
+
chdir: repo, env: default_fizzy_env)
|
|
240
|
+
rescue StandardError => e
|
|
241
|
+
LOG.warn "[Fizzy] Could not add 🛑 reaction: #{e.message}"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
LOG.info "[Fizzy] Cancel command received for card #{card_number_for_cancel || card_internal_id}: killed #{killed} session(s)"
|
|
246
|
+
[200, { status: "cancelled", card: card_number_for_cancel || card_internal_id, sessions_killed: killed }.to_json]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def resolve_fizzy_project(card_info, card_internal_id, eventable)
|
|
250
|
+
if card_info
|
|
251
|
+
if card_info["project"]
|
|
252
|
+
project_key = card_info["project"]
|
|
253
|
+
project_config = PROJECTS[project_key] || DEFAULT_PROJECT
|
|
254
|
+
else
|
|
255
|
+
card_tags = eventable.dig("card", "tags") || []
|
|
256
|
+
project_result = identify_project_by_tags(card_tags)
|
|
257
|
+
if project_result
|
|
258
|
+
project_key, project_config = project_result
|
|
259
|
+
card_info["project"] = project_key
|
|
260
|
+
map = load_work_item_map
|
|
261
|
+
map[card_internal_id] = card_info
|
|
262
|
+
save_work_item_map(map)
|
|
263
|
+
LOG.info "Backfilled project '#{project_key}' for card #{card_internal_id} in card map"
|
|
264
|
+
else
|
|
265
|
+
LOG.warn "No project found for card #{card_internal_id}"
|
|
266
|
+
return [nil, nil]
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
else
|
|
270
|
+
card_tags = eventable.dig("card", "tags") || []
|
|
271
|
+
project_result = identify_project_by_tags(card_tags)
|
|
272
|
+
if project_result
|
|
273
|
+
project_key, project_config = project_result
|
|
274
|
+
else
|
|
275
|
+
LOG.warn "No project found for mentioned card #{card_internal_id}"
|
|
276
|
+
return [nil, nil]
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
[project_config, project_key]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def resolve_comment_agent(mentioned_agent:, card_info:, card_internal_id:, eventable:, project_config:, creator_is_agent:)
|
|
284
|
+
card_assigned_agent = card_info&.dig("agent")
|
|
285
|
+
|
|
286
|
+
# Resolve assigned agent from payload or API if missing
|
|
287
|
+
card_assigned_agent = resolve_assigned_agent(card_info, card_internal_id, eventable, project_config) if card_assigned_agent.nil?
|
|
288
|
+
|
|
289
|
+
if mentioned_agent
|
|
290
|
+
agent_name = mentioned_agent
|
|
291
|
+
is_cross_agent_mention = !card_assigned_agent || card_assigned_agent != mentioned_agent
|
|
292
|
+
else
|
|
293
|
+
unless card_assigned_agent
|
|
294
|
+
LOG.info "Skipping card #{card_internal_id} — no assigned agent and no mention"
|
|
295
|
+
return [nil, false]
|
|
296
|
+
end
|
|
297
|
+
agent_name = card_assigned_agent
|
|
298
|
+
is_cross_agent_mention = false
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
[agent_name, is_cross_agent_mention]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def resolve_assigned_agent(card_info, card_internal_id, eventable, project_config)
|
|
305
|
+
card_assignees = eventable.dig("card", "assignees") || []
|
|
306
|
+
webhook_agent = card_assignees.map { |a| a["name"] }.find { |name| local_agent_names.include?(name) }
|
|
307
|
+
|
|
308
|
+
webhook_agent = resolve_agent_via_api(card_info, card_internal_id, project_config) if webhook_agent.nil? && project_config
|
|
309
|
+
|
|
310
|
+
return nil unless webhook_agent
|
|
311
|
+
|
|
312
|
+
map = load_work_item_map
|
|
313
|
+
map[card_internal_id] ||= {}
|
|
314
|
+
map[card_internal_id]["agent"] = webhook_agent
|
|
315
|
+
save_work_item_map(map)
|
|
316
|
+
LOG.info "Backfilled agent '#{webhook_agent}' into card map for #{card_internal_id}"
|
|
317
|
+
webhook_agent
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def resolve_agent_via_api(card_info, card_internal_id, project_config)
|
|
321
|
+
api_card_number = card_info&.dig("number") || card_internal_id
|
|
322
|
+
return nil unless api_card_number
|
|
323
|
+
|
|
324
|
+
output = run_cmd("fizzy", "card", "show", api_card_number.to_s,
|
|
325
|
+
chdir: project_config["repo_path"], env: default_fizzy_env)
|
|
326
|
+
api_assignees = begin
|
|
327
|
+
JSON.parse(output).dig("data", "assignees") || []
|
|
328
|
+
rescue StandardError
|
|
329
|
+
[]
|
|
330
|
+
end
|
|
331
|
+
agent = api_assignees.map { |a| a["name"] }.find { |name| local_agent_names.include?(name) }
|
|
332
|
+
LOG.info "Resolved assigned agent '#{agent}' via Fizzy API for card ##{api_card_number}" if agent
|
|
333
|
+
agent
|
|
334
|
+
rescue StandardError => e
|
|
335
|
+
LOG.warn "Fizzy API fallback failed for card ##{api_card_number}: #{e.message}"
|
|
336
|
+
nil
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Handle cross-agent mention (agent tagged on another agent's card)
|
|
340
|
+
def handle_cross_agent_mention(ctx)
|
|
341
|
+
card_assigned_agent = ctx.card_info&.dig("agent")
|
|
342
|
+
return [200, { status: "ignored", reason: "card creation announcement" }.to_json] if cross_agent_announcement?(ctx)
|
|
343
|
+
|
|
344
|
+
card_number = ctx.card_info&.dig("number")
|
|
345
|
+
card_number ||= resolve_card_number(ctx.card_internal_id, repo_path: ctx.project_config["repo_path"])
|
|
346
|
+
card_key = "card-#{card_number || ctx.card_internal_id}-#{ctx.agent_name.downcase}"
|
|
347
|
+
if ctx.creator_is_agent && session_active?(card_key)
|
|
348
|
+
return [200, { status: "ignored", reason: "session wait timeout" }.to_json] unless wait_for_session?(card_key)
|
|
349
|
+
elsif session_active?(card_key)
|
|
350
|
+
return [200, { status: "ignored", reason: "session already active" }.to_json]
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
LOG.info "Cross-agent mention: #{ctx.agent_name} tagged on #{card_assigned_agent}'s card " \
|
|
354
|
+
"##{card_number || ctx.card_internal_id} (project: #{ctx.project_key})"
|
|
355
|
+
record_agent_dispatch(ctx.card_internal_id) if ctx.creator_is_agent
|
|
356
|
+
|
|
357
|
+
react_to_comment(card_number, ctx.comment_id, ctx.project_config, ctx.agent_name, "👀")
|
|
358
|
+
|
|
359
|
+
dispatch_cross_agent_review(ctx, card_key: card_key, card_number: card_number,
|
|
360
|
+
card_assigned_agent: card_assigned_agent)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def dispatch_cross_agent_review(ctx, card_key:, card_number:, card_assigned_agent:)
|
|
364
|
+
review_worktree_path, review_branch = setup_cross_agent_worktree(ctx, card_number)
|
|
365
|
+
prompt = build_cross_agent_prompt(ctx, card_number, card_assigned_agent, review_worktree_path, review_branch)
|
|
366
|
+
|
|
367
|
+
pid, log_file = run_agent(prompt,
|
|
368
|
+
project_config: ctx.project_config, chdir: review_worktree_path,
|
|
369
|
+
log_name: "review-#{ctx.agent_name.downcase}-#{card_number || ctx.card_internal_id}",
|
|
370
|
+
model: ctx.model, effort: ctx.effort, agent_name: ctx.agent_name,
|
|
371
|
+
card_number: card_number, comment_id: ctx.comment_id,
|
|
372
|
+
source: :fizzy, source_context: { card_number: card_number },
|
|
373
|
+
cli_provider: ctx.cli_provider_override)
|
|
374
|
+
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: ctx.agent_name)
|
|
375
|
+
|
|
376
|
+
[200, { status: "cross_agent_review", agent: ctx.agent_name, card_agent: card_assigned_agent,
|
|
377
|
+
card: card_number, card_internal_id: ctx.card_internal_id,
|
|
378
|
+
project: ctx.project_key, worktree: review_worktree_path }.to_json]
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def build_cross_agent_prompt(ctx, card_number, card_assigned_agent, worktree_path, branch)
|
|
382
|
+
card_context = prefetch_card_context(card_number, repo_path: ctx.project_config["repo_path"], agent_name: ctx.agent_name)
|
|
383
|
+
|
|
384
|
+
render_prompt(PROMPT_CROSS_AGENT_REVIEW,
|
|
385
|
+
ctx.comment_vars.merge(
|
|
386
|
+
"CARD_NUMBER" => card_number || "N/A",
|
|
387
|
+
"CARD_INTERNAL_ID" => ctx.card_internal_id,
|
|
388
|
+
"CARD_ID" => card_number || ctx.card_internal_id,
|
|
389
|
+
"CARD_AGENT" => card_assigned_agent,
|
|
390
|
+
"WORKTREE_PATH" => worktree_path,
|
|
391
|
+
"BRANCH" => branch
|
|
392
|
+
),
|
|
393
|
+
brain_context: build_brain_context(
|
|
394
|
+
agent_name: ctx.agent_name, card_number: card_number,
|
|
395
|
+
project_key: ctx.project_key, comment_body: ctx.plain_text, source: :fizzy
|
|
396
|
+
),
|
|
397
|
+
card_context: card_context,
|
|
398
|
+
agent_name: ctx.agent_name)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Handle comment on a card that's already in the card map (or has a worktree override)
|
|
402
|
+
def handle_existing_card_comment(ctx)
|
|
403
|
+
effective_info = ctx.worktree_override ? (ctx.card_info || {}).merge(ctx.worktree_override) : ctx.card_info
|
|
404
|
+
card_number = effective_info["number"]
|
|
405
|
+
worktree = effective_info["worktree"]
|
|
406
|
+
|
|
407
|
+
card_number = resolve_and_save_card_number(ctx.card_internal_id, ctx.project_config) if card_number.nil?
|
|
408
|
+
worktree = find_and_save_worktree(ctx.card_internal_id, card_number, ctx.project_config) if !(worktree && File.directory?(worktree)) && card_number
|
|
409
|
+
|
|
410
|
+
work_dir = worktree && File.directory?(worktree) ? worktree : ctx.project_config["repo_path"]
|
|
411
|
+
card_key = "card-#{card_number || ctx.card_internal_id}"
|
|
412
|
+
|
|
413
|
+
# Session management (wait, supersede, or queue)
|
|
414
|
+
queued = handle_session_conflict(ctx, card_key, card_number, work_dir)
|
|
415
|
+
return queued if queued
|
|
416
|
+
|
|
417
|
+
LOG.info "Follow-up comment on card #{card_number || ctx.card_internal_id} " \
|
|
418
|
+
"(project: #{ctx.project_key}), worktree: #{work_dir}"
|
|
419
|
+
|
|
420
|
+
react_to_comment(card_number, ctx.comment_id, ctx.project_config, ctx.agent_name, "👍", chdir: work_dir)
|
|
421
|
+
|
|
422
|
+
result = dispatch_followup_comment(ctx, card_key: card_key, card_number: card_number, work_dir: work_dir)
|
|
423
|
+
[200, result.to_json]
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Handle mention on a card with no existing card_info (exploration)
|
|
427
|
+
def handle_new_mention(ctx)
|
|
428
|
+
card_data = ctx.eventable["card"] || {}
|
|
429
|
+
card_number = card_data["number"]
|
|
430
|
+
card_title = card_data["title"] || "exploration"
|
|
431
|
+
|
|
432
|
+
if card_number.nil?
|
|
433
|
+
map_entry = load_work_item_map[ctx.card_internal_id]
|
|
434
|
+
card_number = if map_entry && map_entry["number"]
|
|
435
|
+
map_entry["number"]
|
|
436
|
+
else
|
|
437
|
+
resolve_card_number(ctx.card_internal_id, repo_path: ctx.project_config["repo_path"])
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
LOG.info "#{ctx.agent_name} mentioned on card (internal_id: #{ctx.card_internal_id}, " \
|
|
442
|
+
"project: #{ctx.project_key}), creating exploration worktree"
|
|
443
|
+
record_agent_dispatch(ctx.card_internal_id) if ctx.creator_is_agent
|
|
444
|
+
|
|
445
|
+
card_key = "card-#{card_number || ctx.card_internal_id}"
|
|
446
|
+
return [200, { status: "ignored", reason: "session already active" }.to_json] if session_active?(card_key)
|
|
447
|
+
|
|
448
|
+
react_to_comment(card_number, ctx.comment_id, ctx.project_config, ctx.agent_name, "👀")
|
|
449
|
+
|
|
450
|
+
worktree_path, branch = setup_new_mention_worktree(ctx, card_number, card_title)
|
|
451
|
+
dispatch_new_mention(ctx, card_key: card_key, card_number: card_number,
|
|
452
|
+
card_title: card_title, branch: branch, worktree_path: worktree_path)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def setup_new_mention_worktree(ctx, card_number, card_title)
|
|
456
|
+
repo_path = ctx.project_config["repo_path"]
|
|
457
|
+
worktree_path, branch = resolve_or_create_worktree(ctx, card_number, card_title, repo_path)
|
|
458
|
+
|
|
459
|
+
map = load_work_item_map
|
|
460
|
+
map[ctx.card_internal_id] = {
|
|
461
|
+
"number" => card_number, "branch" => branch, "worktree" => worktree_path,
|
|
462
|
+
"project" => ctx.project_key, "agent" => ctx.agent_name
|
|
463
|
+
}
|
|
464
|
+
save_work_item_map(map)
|
|
465
|
+
|
|
466
|
+
[worktree_path, branch]
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def dispatch_new_mention(ctx, card_key:, card_number:, card_title:, branch:, worktree_path:)
|
|
470
|
+
prompt = build_mention_prompt(ctx, card_number, card_title, branch, worktree_path)
|
|
471
|
+
|
|
472
|
+
pid, log_file = run_agent(prompt,
|
|
473
|
+
project_config: ctx.project_config, chdir: worktree_path,
|
|
474
|
+
log_name: "mention-#{card_number || ctx.card_internal_id}",
|
|
475
|
+
model: ctx.model, effort: ctx.effort, agent_name: ctx.agent_name,
|
|
476
|
+
card_number: card_number, comment_id: ctx.comment_id,
|
|
477
|
+
source: :fizzy, cli_provider: ctx.cli_provider_override,
|
|
478
|
+
source_context: { card_number: card_number })
|
|
479
|
+
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: ctx.agent_name)
|
|
480
|
+
|
|
481
|
+
[200, { status: "responded", card_internal_id: ctx.card_internal_id, card_number: card_number,
|
|
482
|
+
branch: branch, worktree: worktree_path, project: ctx.project_key }.to_json]
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Dispatch a follow-up comment to the agent.
|
|
486
|
+
def dispatch_followup_comment(ctx, card_key:, card_number:, work_dir:)
|
|
487
|
+
card_tags = ctx.eventable.dig("card", "tags") || []
|
|
488
|
+
effort = detect_effort(ctx.project_config, tags: card_tags, text: ctx.plain_text)
|
|
489
|
+
|
|
490
|
+
is_worktree = work_dir != ctx.project_config["repo_path"]
|
|
491
|
+
resolved = resolve_project_cli_config(ctx.project_config,
|
|
492
|
+
cli_provider_override: ctx.cli_provider_override,
|
|
493
|
+
agent_name: ctx.agent_name)
|
|
494
|
+
should_resume = is_worktree && resolved["resume_flag"]
|
|
495
|
+
|
|
496
|
+
prompt = if should_resume
|
|
497
|
+
LOG.info "[Resume] Using lean prompt for follow-up on card #{card_number || ctx.card_internal_id}"
|
|
498
|
+
render_resume_prompt(
|
|
499
|
+
comment_body: ctx.plain_text, comment_creator: ctx.comment_vars["COMMENT_CREATOR"],
|
|
500
|
+
comment_id: ctx.comment_id, card_number: card_number, agent_name: ctx.agent_name
|
|
501
|
+
)
|
|
502
|
+
else
|
|
503
|
+
build_followup_prompt(ctx, card_number, card_tags, work_dir)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
pid, log_file = run_agent(prompt,
|
|
507
|
+
project_config: ctx.project_config, chdir: work_dir,
|
|
508
|
+
log_name: "followup-#{card_number || ctx.card_internal_id}",
|
|
509
|
+
model: ctx.model, effort: effort, agent_name: ctx.agent_name,
|
|
510
|
+
card_number: card_number, comment_id: ctx.comment_id,
|
|
511
|
+
source: :fizzy, cli_provider: ctx.cli_provider_override, resume: is_worktree,
|
|
512
|
+
source_context: {
|
|
513
|
+
card_number: card_number, card_internal_id: ctx.card_internal_id,
|
|
514
|
+
deploy_intent: ctx.deploy_intent
|
|
515
|
+
})
|
|
516
|
+
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: ctx.agent_name)
|
|
517
|
+
|
|
518
|
+
Thread.new { move_card_to_column(card_number, "right_now", project_config: ctx.project_config, agent_name: ctx.agent_name) }
|
|
519
|
+
|
|
520
|
+
{ status: "follow_up", card: card_number, card_internal_id: ctx.card_internal_id,
|
|
521
|
+
worktree: work_dir, project: ctx.project_key }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# --- Shared helpers ---
|
|
525
|
+
|
|
526
|
+
def card_announcement?(text)
|
|
527
|
+
text.match?(/created\s+card\s+#?\d+/i) ||
|
|
528
|
+
text.match?(/assigned\s+.*card\s+#?\d+/i) ||
|
|
529
|
+
text.match?(/card\s+#?\d+.*assigned/i)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def cross_agent_announcement?(ctx)
|
|
533
|
+
return false unless ctx.creator_is_agent && card_announcement?(ctx.plain_text)
|
|
534
|
+
|
|
535
|
+
LOG.info "Ignoring cross-agent mention from #{ctx.comment_vars["COMMENT_CREATOR"]} " \
|
|
536
|
+
"on card #{ctx.card_internal_id} — card creation/assignment (handled by webhook)"
|
|
537
|
+
true
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def react_to_comment(card_number, comment_id, project_config, agent_name, emoji, chdir: nil)
|
|
541
|
+
return unless card_number
|
|
542
|
+
|
|
543
|
+
work_dir = chdir || project_config["repo_path"]
|
|
544
|
+
Thread.new do
|
|
545
|
+
run_cmd("fizzy", "reaction", "create", "--card", card_number.to_s,
|
|
546
|
+
"--comment", comment_id.to_s, "--content", emoji,
|
|
547
|
+
chdir: work_dir, env: fizzy_env_for(agent_name))
|
|
548
|
+
rescue StandardError => e
|
|
549
|
+
LOG.warn "Could not add #{emoji} reaction to comment: #{e.message}"
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def resolve_and_save_card_number(card_internal_id, project_config)
|
|
554
|
+
card_number = resolve_card_number(card_internal_id, repo_path: project_config["repo_path"])
|
|
555
|
+
if card_number
|
|
556
|
+
map = load_work_item_map
|
|
557
|
+
map[card_internal_id] ||= {}
|
|
558
|
+
map[card_internal_id]["number"] = card_number
|
|
559
|
+
save_work_item_map(map)
|
|
560
|
+
end
|
|
561
|
+
card_number
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def find_and_save_worktree(card_internal_id, card_number, project_config)
|
|
565
|
+
found = find_worktree_for_card(card_number, repo_path: project_config["repo_path"])
|
|
566
|
+
return nil unless found
|
|
567
|
+
|
|
568
|
+
map = load_work_item_map
|
|
569
|
+
map[card_internal_id] ||= {}
|
|
570
|
+
map[card_internal_id].merge!("worktree" => found[:worktree], "branch" => found[:branch])
|
|
571
|
+
save_work_item_map(map)
|
|
572
|
+
LOG.info "Found worktree by card number scan: #{found[:worktree]}"
|
|
573
|
+
found[:worktree]
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def handle_session_conflict(ctx, card_key, card_number, work_dir)
|
|
577
|
+
if ctx.creator_is_agent && session_active?(card_key)
|
|
578
|
+
return [200, { status: "ignored", reason: "session wait timeout" }.to_json] unless wait_for_session?(card_key)
|
|
579
|
+
elsif session_active?(card_key)
|
|
580
|
+
prev = find_supersedable_session(card_key)
|
|
581
|
+
return queue_followup(ctx, card_key, card_number, work_dir) unless prev
|
|
582
|
+
|
|
583
|
+
LOG.info "Superseding session on card #{card_number || ctx.card_internal_id} " \
|
|
584
|
+
"(pid: #{prev[:pid]}) — human follow-up within #{SUPERSEDE_WINDOW}s"
|
|
585
|
+
kill_session(prev[:session_key])
|
|
586
|
+
|
|
587
|
+
end
|
|
588
|
+
nil
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def queue_followup(ctx, card_key, card_number, work_dir)
|
|
592
|
+
react_to_comment(card_number, ctx.comment_id, ctx.project_config, ctx.agent_name, "👍", chdir: work_dir)
|
|
593
|
+
|
|
594
|
+
Thread.new do
|
|
595
|
+
unless wait_for_session?(card_key)
|
|
596
|
+
LOG.warn "Giving up on queued follow-up for card #{card_number || ctx.card_internal_id}"
|
|
597
|
+
next
|
|
598
|
+
end
|
|
599
|
+
dispatch_followup_comment(ctx, card_key: card_key, card_number: card_number, work_dir: work_dir)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
[200, { status: "queued", card: card_number, card_internal_id: ctx.card_internal_id,
|
|
603
|
+
reason: "waiting for active session" }.to_json]
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def setup_cross_agent_worktree(ctx, card_number)
|
|
607
|
+
repo_path = ctx.project_config["repo_path"]
|
|
608
|
+
card_title = ctx.card_info&.dig("title") || ctx.eventable.dig("card", "title") || "review"
|
|
609
|
+
review_branch = "#{ctx.agent_name.downcase}/fizzy-#{card_number}-#{slugify(card_title)}"
|
|
610
|
+
review_worktree_path = File.join(File.dirname(repo_path), "#{File.basename(repo_path)}--#{review_branch.tr("/", "-")}")
|
|
611
|
+
|
|
612
|
+
debounced_repo_fetch(repo_path)
|
|
613
|
+
|
|
614
|
+
if File.directory?(review_worktree_path)
|
|
615
|
+
worktree_list = run_cmd("git", "worktree", "list", "--porcelain", chdir: repo_path)
|
|
616
|
+
FileUtils.rm_rf(review_worktree_path) unless worktree_list.include?(review_worktree_path)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
create_review_worktree(repo_path, review_branch, review_worktree_path, ctx.card_info) unless File.directory?(review_worktree_path)
|
|
620
|
+
|
|
621
|
+
[review_worktree_path, review_branch]
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def create_review_worktree(repo_path, review_branch, review_worktree_path, card_info)
|
|
625
|
+
card_branch = card_info&.dig("branch")
|
|
626
|
+
branch_exists = card_branch && system("git", "rev-parse", "--verify", card_branch,
|
|
627
|
+
chdir: repo_path, out: File::NULL, err: File::NULL)
|
|
628
|
+
base_ref = branch_exists ? card_branch : "origin/#{get_default_branch(repo_path)}"
|
|
629
|
+
|
|
630
|
+
if system("git", "rev-parse", "--verify", review_branch, chdir: repo_path, out: File::NULL, err: File::NULL)
|
|
631
|
+
run_cmd("git", "branch", "-D", review_branch, chdir: repo_path)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
run_cmd("git", "worktree", "add", "-b", review_branch, review_worktree_path, base_ref, chdir: repo_path)
|
|
635
|
+
trust_version_manager(review_worktree_path, chdir: review_worktree_path)
|
|
636
|
+
apply_worktree_includes(repo_path, review_worktree_path)
|
|
637
|
+
run_project_hook(repo_path, "worktree-setup", extra_env: { "WORKTREE_PATH" => review_worktree_path })
|
|
638
|
+
LOG.info "Created cross-agent review worktree at #{review_worktree_path} (base: #{base_ref})"
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def resolve_or_create_worktree(ctx, card_number, card_title, repo_path)
|
|
642
|
+
# Check for existing worktree in card map or on disk
|
|
643
|
+
existing_map_entry = load_work_item_map[ctx.card_internal_id]
|
|
644
|
+
if existing_map_entry && existing_map_entry["branch"] && existing_map_entry["worktree"] &&
|
|
645
|
+
File.directory?(existing_map_entry["worktree"])
|
|
646
|
+
LOG.info "Reusing existing worktree from card map: #{existing_map_entry["worktree"]}"
|
|
647
|
+
return [existing_map_entry["worktree"], existing_map_entry["branch"]]
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
if card_number
|
|
651
|
+
found = find_worktree_for_card(card_number, repo_path: repo_path)
|
|
652
|
+
if found
|
|
653
|
+
LOG.info "Found existing worktree by card number scan: #{found[:worktree]}"
|
|
654
|
+
return [found[:worktree], found[:branch]]
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
branch = card_number ? "fizzy-#{card_number}-#{slugify(card_title)}" : "fizzy-explore-#{ctx.card_internal_id[0..7]}"
|
|
659
|
+
debounced_repo_fetch(repo_path)
|
|
660
|
+
worktree_path = create_or_reuse_worktree(repo_path: repo_path, branch: branch)
|
|
661
|
+
[worktree_path, branch]
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def build_mention_prompt(ctx, card_number, card_title, branch, worktree_path)
|
|
665
|
+
planning_info = detect_planning_mode(text: ctx.plain_text, tags: ctx.card_tags,
|
|
666
|
+
card_internal_id: ctx.card_internal_id, card_number: card_number)
|
|
667
|
+
|
|
668
|
+
if planning_info
|
|
669
|
+
render_planning_prompt(PROMPT_MENTION,
|
|
670
|
+
ctx.comment_vars.merge(
|
|
671
|
+
"CARD_INTERNAL_ID" => ctx.card_internal_id, "CARD_ID" => planning_info[:card_id],
|
|
672
|
+
"CARD_NUMBER" => card_number || "N/A",
|
|
673
|
+
"CARD_NUMBER_TEXT" => card_number ? " (##{card_number})" : "",
|
|
674
|
+
"BRANCH" => branch
|
|
675
|
+
),
|
|
676
|
+
brain_context: build_brain_context(
|
|
677
|
+
agent_name: ctx.agent_name, card_title: card_title, card_number: card_number,
|
|
678
|
+
project_key: ctx.project_key, comment_body: ctx.plain_text, source: :fizzy
|
|
679
|
+
),
|
|
680
|
+
card_context: prefetch_card_context(card_number, repo_path: worktree_path,
|
|
681
|
+
agent_name: ctx.agent_name),
|
|
682
|
+
agent_name: ctx.agent_name)
|
|
683
|
+
else
|
|
684
|
+
card_id = card_number || ctx.card_internal_id
|
|
685
|
+
render_prompt(PROMPT_MENTION,
|
|
686
|
+
ctx.comment_vars.merge(
|
|
687
|
+
"CARD_INTERNAL_ID" => ctx.card_internal_id, "CARD_ID" => card_id,
|
|
688
|
+
"CARD_NUMBER" => card_number || "N/A", "CARD_NUMBER_TEXT" => card_number || ctx.card_internal_id
|
|
689
|
+
),
|
|
690
|
+
brain_context: build_brain_context(
|
|
691
|
+
agent_name: ctx.agent_name, card_title: card_title, card_number: card_number,
|
|
692
|
+
project_key: ctx.project_key, comment_body: ctx.plain_text, source: :fizzy
|
|
693
|
+
),
|
|
694
|
+
card_context: prefetch_card_context(card_number, repo_path: worktree_path,
|
|
695
|
+
agent_name: ctx.agent_name),
|
|
696
|
+
agent_name: ctx.agent_name)
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def build_followup_prompt(ctx, card_number, card_tags, work_dir)
|
|
701
|
+
planning_info = detect_planning_mode(text: ctx.plain_text, tags: card_tags,
|
|
702
|
+
card_internal_id: ctx.card_internal_id, card_number: card_number)
|
|
703
|
+
|
|
704
|
+
if planning_info
|
|
705
|
+
build_planning_followup_prompt(ctx, card_number, planning_info[:card_id], work_dir)
|
|
706
|
+
elsif work_dir != ctx.project_config["repo_path"]
|
|
707
|
+
render_prompt(PROMPT_FOLLOWUP_WORKTREE,
|
|
708
|
+
ctx.comment_vars.merge("CARD_NUMBER" => card_number, "CARD_ID" => card_number),
|
|
709
|
+
brain_context: build_brain_context(
|
|
710
|
+
agent_name: ctx.agent_name, card_number: card_number,
|
|
711
|
+
project_key: ctx.project_key, comment_body: ctx.plain_text, source: :fizzy
|
|
712
|
+
),
|
|
713
|
+
card_context: prefetch_card_context(card_number, repo_path: work_dir, agent_name: ctx.agent_name),
|
|
714
|
+
agent_name: ctx.agent_name)
|
|
715
|
+
else
|
|
716
|
+
render_prompt(PROMPT_FOLLOWUP_NO_WORKTREE,
|
|
717
|
+
ctx.comment_vars.merge("CARD_INTERNAL_ID" => ctx.card_internal_id, "CARD_ID" => ctx.card_internal_id),
|
|
718
|
+
brain_context: build_brain_context(
|
|
719
|
+
agent_name: ctx.agent_name, project_key: ctx.project_key,
|
|
720
|
+
comment_body: ctx.plain_text, source: :fizzy
|
|
721
|
+
),
|
|
722
|
+
card_context: prefetch_card_context(card_number, repo_path: ctx.project_config["repo_path"],
|
|
723
|
+
agent_name: ctx.agent_name),
|
|
724
|
+
agent_name: ctx.agent_name)
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def build_planning_followup_prompt(ctx, card_number, card_id, work_dir)
|
|
729
|
+
if work_dir == ctx.project_config["repo_path"]
|
|
730
|
+
render_planning_prompt(PROMPT_FOLLOWUP_NO_WORKTREE,
|
|
731
|
+
ctx.comment_vars.merge("CARD_INTERNAL_ID" => ctx.card_internal_id, "CARD_ID" => card_id),
|
|
732
|
+
brain_context: build_brain_context(
|
|
733
|
+
agent_name: ctx.agent_name, project_key: ctx.project_key,
|
|
734
|
+
comment_body: ctx.plain_text, source: :fizzy
|
|
735
|
+
),
|
|
736
|
+
card_context: prefetch_card_context(card_number, repo_path: ctx.project_config["repo_path"],
|
|
737
|
+
agent_name: ctx.agent_name),
|
|
738
|
+
agent_name: ctx.agent_name)
|
|
739
|
+
else
|
|
740
|
+
render_planning_prompt(PROMPT_FOLLOWUP_WORKTREE,
|
|
741
|
+
ctx.comment_vars.merge("CARD_NUMBER" => card_number, "CARD_ID" => card_id),
|
|
742
|
+
brain_context: build_brain_context(
|
|
743
|
+
agent_name: ctx.agent_name, card_number: card_number,
|
|
744
|
+
project_key: ctx.project_key, comment_body: ctx.plain_text, source: :fizzy
|
|
745
|
+
),
|
|
746
|
+
card_context: prefetch_card_context(card_number, repo_path: work_dir, agent_name: ctx.agent_name),
|
|
747
|
+
agent_name: ctx.agent_name)
|
|
748
|
+
end
|
|
749
|
+
end
|