brainiac-fizzy 0.0.24 → 0.0.26
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/lib/brainiac/plugins/fizzy/cli.rb +67 -2
- data/lib/brainiac/plugins/fizzy/delegators.rb +16 -0
- data/lib/brainiac/plugins/fizzy/handlers/assignment.rb +144 -4
- data/lib/brainiac/plugins/fizzy/handlers/comments.rb +6 -0
- data/lib/brainiac/plugins/fizzy/helpers.rb +47 -0
- data/lib/brainiac/plugins/fizzy/hooks.rb +47 -1
- data/lib/brainiac/plugins/fizzy/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b9d6e5b3e6e6ea9306dc6fd9ce800dffc01e46f99b05f2861e0acf096793fe5
|
|
4
|
+
data.tar.gz: ec15a0b6f7a498de7662b8bd662fb519ae45f8a14d66e5912cf6dd540cc3726b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51284c7c4cca09f1bde28d3589fc7c17b29314e898f4de8bd7345fa1fd930baf6501ef47e42a241b40f17baede635780676154e7c6f456a70ea6f1d9902da59b
|
|
7
|
+
data.tar.gz: 868fc4f7e922d67138125385c486f87bc221766ddadf45e640ad880ee57914f04a772bdd3b688087c505a66cfa59f7643e1134f5731eb0ef67f3e42b6f80a700
|
|
@@ -658,8 +658,73 @@ module Brainiac
|
|
|
658
658
|
end
|
|
659
659
|
|
|
660
660
|
# Subcommand names for bash completion.
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
# When called with no args, returns top-level subcommands.
|
|
662
|
+
# When called with args (the words typed so far after the plugin name),
|
|
663
|
+
# returns context-sensitive completions for nested subcommands.
|
|
664
|
+
def self.completions(args = [])
|
|
665
|
+
return %w[board config setup status] if args.empty?
|
|
666
|
+
|
|
667
|
+
case args[0]
|
|
668
|
+
when "board"
|
|
669
|
+
board_completions(args[1..])
|
|
670
|
+
else
|
|
671
|
+
[]
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Nested completions for `brainiac fizzy board ...`
|
|
676
|
+
def self.board_completions(args)
|
|
677
|
+
board_subcommands = %w[assign columns list ls setup webhook]
|
|
678
|
+
return board_subcommands if args.empty?
|
|
679
|
+
|
|
680
|
+
case args[0]
|
|
681
|
+
when "assign"
|
|
682
|
+
board_assign_completions(args[1..])
|
|
683
|
+
when "columns", "webhook"
|
|
684
|
+
board_key_completions(args[1..])
|
|
685
|
+
else
|
|
686
|
+
[]
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Completions for `brainiac fizzy board assign <project> <board>`
|
|
691
|
+
def self.board_assign_completions(args)
|
|
692
|
+
case args.length
|
|
693
|
+
when 0
|
|
694
|
+
load_project_keys
|
|
695
|
+
when 1
|
|
696
|
+
load_board_keys
|
|
697
|
+
else
|
|
698
|
+
[]
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Completions for commands that take a board key as next arg
|
|
703
|
+
def self.board_key_completions(args)
|
|
704
|
+
return load_board_keys if args.empty?
|
|
705
|
+
|
|
706
|
+
[]
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# Load board keys from fizzy.json for completion
|
|
710
|
+
def self.load_board_keys
|
|
711
|
+
config_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "fizzy.json")
|
|
712
|
+
return [] unless File.exist?(config_file)
|
|
713
|
+
|
|
714
|
+
config = JSON.parse(File.read(config_file))
|
|
715
|
+
(config["boards"] || {}).keys
|
|
716
|
+
rescue StandardError
|
|
717
|
+
[]
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Load project keys from projects.json for completion
|
|
721
|
+
def self.load_project_keys
|
|
722
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
723
|
+
return [] unless File.exist?(projects_file)
|
|
724
|
+
|
|
725
|
+
JSON.parse(File.read(projects_file)).keys
|
|
726
|
+
rescue StandardError
|
|
727
|
+
[]
|
|
663
728
|
end
|
|
664
729
|
|
|
665
730
|
# Called by brainiac CLI after `agent create` — prompts for Fizzy user ID.
|
|
@@ -25,6 +25,22 @@ def prefetch_card_context(card_number, repo_path:, agent_name: nil)
|
|
|
25
25
|
Brainiac::Plugins::Fizzy::Helpers.prefetch_card_context(card_number, repo_path: repo_path, agent_name: agent_name)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def tag_names(tags)
|
|
29
|
+
Brainiac::Plugins::Fizzy::Helpers.tag_names(tags)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def card_has_tag?(tags, name)
|
|
33
|
+
Brainiac::Plugins::Fizzy::Helpers.card_has_tag?(tags, name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_card_tags(card_number, repo_path:, env: nil)
|
|
37
|
+
Brainiac::Plugins::Fizzy::Helpers.fetch_card_tags(card_number, repo_path: repo_path, env: env)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve_github_agent_env(agent_name, github_repo)
|
|
41
|
+
Brainiac::Plugins::Fizzy::Helpers.resolve_github_agent_env(agent_name, github_repo)
|
|
42
|
+
end
|
|
43
|
+
|
|
28
44
|
def fetch_intent_context(card_number, repo_path:, agent_name: nil)
|
|
29
45
|
Brainiac::Plugins::Fizzy::Helpers.fetch_intent_context(card_number, repo_path: repo_path, agent_name: agent_name)
|
|
30
46
|
end
|
|
@@ -58,6 +58,10 @@ def handle_card_assigned(payload, board_key: nil)
|
|
|
58
58
|
inline_effort: initial_effort
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
maybe_create_ephemeral_belt_env(
|
|
62
|
+
worktree_path: worktree_path, card_number: card_number, project_key: project_key, tags: tags
|
|
63
|
+
)
|
|
64
|
+
|
|
61
65
|
dispatch_assigned_card(
|
|
62
66
|
card_number: card_number, card_internal_id: card_internal_id, title: title, tags: tags,
|
|
63
67
|
branch: branch, worktree_path: worktree_path, project_config: project_config, project_key: project_key,
|
|
@@ -142,9 +146,7 @@ def dispatch_assigned_card(card_number:, card_internal_id:, title:, tags:, branc
|
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
# Resolve custom PR target (for epic branch workflows)
|
|
145
|
-
pr_target =
|
|
146
|
-
resolve_pr_target(repo_path: project_config["repo_path"], card_number: card_number, project_key: project_key)
|
|
147
|
-
end
|
|
149
|
+
pr_target = resolve_pr_target(repo_path: project_config["repo_path"], card_number: card_number, project_key: project_key)
|
|
148
150
|
template_vars["PR_TARGET_INSTRUCTION"] = if pr_target
|
|
149
151
|
"**IMPORTANT: Open the PR targeting the `#{pr_target}` branch " \
|
|
150
152
|
"(not main).** Use: `gh pr create --base #{pr_target}`"
|
|
@@ -168,15 +170,153 @@ def dispatch_assigned_card(card_number:, card_internal_id:, title:, tags:, branc
|
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
card_key = "card-#{card_number}"
|
|
173
|
+
|
|
174
|
+
# Inject GitHub App token so agent's gh commands run as their bot identity
|
|
175
|
+
github_repo = project_config["github_repo"]
|
|
176
|
+
agent_github_env = resolve_github_agent_env(agent_name, github_repo)
|
|
177
|
+
|
|
171
178
|
pid, log_file = run_agent(prompt,
|
|
172
179
|
project_config: project_config, chdir: worktree_path,
|
|
173
180
|
log_name: "assigned-#{card_number}", model: model, effort: effort,
|
|
174
181
|
agent_name: agent_name, card_number: card_number, source: :fizzy,
|
|
175
182
|
source_context: { card_number: card_number, board_key: board_key, dispatched_at: Time.now },
|
|
176
|
-
cli_provider: cli_provider_override)
|
|
183
|
+
cli_provider: cli_provider_override, env: agent_github_env)
|
|
177
184
|
register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: agent_name)
|
|
178
185
|
|
|
179
186
|
Thread.new { move_card_to_column(card_number, "right_now", project_config: project_config, agent_name: agent_name, board_key: board_key) }
|
|
180
187
|
|
|
181
188
|
[200, { status: "processed", card: card_number, branch: branch, project: project_key, agent: agent_name }.to_json]
|
|
182
189
|
end
|
|
190
|
+
|
|
191
|
+
# Ensure an ephemeral Belt environment is configured in the worktree when the
|
|
192
|
+
# card has a `deploy` tag. Fizzy has no tag-added webhook, so comment handlers
|
|
193
|
+
# pass fetch_live_tags: true to read current tags from `fizzy card show`.
|
|
194
|
+
#
|
|
195
|
+
# `belt g environment` is synchronous so the worktree is configured before the
|
|
196
|
+
# agent starts. The actual `belt deploy` runs in the background.
|
|
197
|
+
def maybe_create_ephemeral_belt_env(worktree_path:, card_number:, project_key:, tags: nil,
|
|
198
|
+
repo_path: nil, agent_name: nil, fetch_live_tags: false)
|
|
199
|
+
unless defined?(BeltConfig) && defined?(BeltEnvironment)
|
|
200
|
+
LOG.debug "[EphemeralEnv] Belt utilities not available — skipping"
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
unless worktree_path && File.directory?(worktree_path)
|
|
205
|
+
LOG.debug "[EphemeralEnv] Worktree missing at #{worktree_path.inspect} — skipping"
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
unless belt_app_for_ephemeral?(worktree_path)
|
|
210
|
+
LOG.debug "[EphemeralEnv] #{project_key} is not a Belt app — skipping"
|
|
211
|
+
return
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
unless BeltConfig.ephemeral_deploys_enabled?
|
|
215
|
+
LOG.info "[EphemeralEnv] Ephemeral deploys disabled in basecamp.json"
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
resolved_tags = resolve_ephemeral_env_tags(
|
|
220
|
+
tags, card_number: card_number, repo_path: repo_path || worktree_path,
|
|
221
|
+
agent_name: agent_name, fetch_live_tags: fetch_live_tags
|
|
222
|
+
)
|
|
223
|
+
tag_list = tag_names(resolved_tags)
|
|
224
|
+
unless tag_list.include?("deploy")
|
|
225
|
+
LOG.debug "[EphemeralEnv] Card ##{card_number} tags=#{tag_list.inspect} — no deploy tag, skipping"
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
parent_env = BeltConfig.parent_env_for(project_key)
|
|
230
|
+
unless parent_env
|
|
231
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag but no parent env configured for #{project_key}"
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
env_name = BeltConfig.ephemeral_env_for_card(card_number)
|
|
236
|
+
env_dir = File.join(worktree_path, "infrastructure", env_name.to_s)
|
|
237
|
+
|
|
238
|
+
if File.directory?(env_dir)
|
|
239
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag; #{env_name} already configured at #{env_dir}"
|
|
240
|
+
track_ephemeral_env_if_needed(env_name, card_number, project_key, parent_env, worktree_path)
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
parent_dir = File.join(worktree_path, "infrastructure", parent_env.to_s)
|
|
245
|
+
unless File.directory?(parent_dir)
|
|
246
|
+
LOG.error "[EphemeralEnv] Parent env '#{parent_env}' not found at #{parent_dir}"
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} has deploy tag; #{env_name} not in worktree — creating from '#{parent_env}'"
|
|
251
|
+
create_and_deploy_ephemeral_env(
|
|
252
|
+
worktree_path: worktree_path, env_name: env_name, parent_env: parent_env,
|
|
253
|
+
card_number: card_number, project_key: project_key
|
|
254
|
+
)
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
LOG.error "[EphemeralEnv] Error creating ephemeral env: #{e.message}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def ensure_ephemeral_env_for_comment(ctx, card_number, worktree)
|
|
260
|
+
return unless card_number && worktree && File.directory?(worktree)
|
|
261
|
+
|
|
262
|
+
maybe_create_ephemeral_belt_env(
|
|
263
|
+
worktree_path: worktree, card_number: card_number, project_key: ctx.project_key,
|
|
264
|
+
tags: ctx.card_tags, repo_path: ctx.project_config["repo_path"],
|
|
265
|
+
agent_name: ctx.agent_name, fetch_live_tags: true
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def belt_app_for_ephemeral?(worktree_path)
|
|
270
|
+
return BeltEnvironment.belt_app?(worktree_path) if defined?(BeltEnvironment) && BeltEnvironment.respond_to?(:belt_app?)
|
|
271
|
+
|
|
272
|
+
%w[config/routes.rb config/routes.tf.rb infrastructure/routes.tf.rb].any? do |rel|
|
|
273
|
+
File.exist?(File.join(worktree_path, rel))
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def resolve_ephemeral_env_tags(tags, card_number:, repo_path:, agent_name:, fetch_live_tags:)
|
|
278
|
+
return tags unless fetch_live_tags && card_number
|
|
279
|
+
|
|
280
|
+
live = fetch_card_tags(card_number, repo_path: repo_path, env: fizzy_env_for(agent_name || AI_AGENT_NAME))
|
|
281
|
+
if live
|
|
282
|
+
LOG.info "[EphemeralEnv] Card ##{card_number} live tags: #{tag_names(live).inspect}"
|
|
283
|
+
live
|
|
284
|
+
else
|
|
285
|
+
LOG.warn "[EphemeralEnv] Could not fetch live tags for card ##{card_number} — falling back to webhook tags #{tag_names(tags).inspect}"
|
|
286
|
+
tags
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def track_ephemeral_env_if_needed(env_name, card_number, project_key, parent_env, worktree_path)
|
|
291
|
+
return if BeltConfig.ephemeral_env?(env_name)
|
|
292
|
+
|
|
293
|
+
BeltConfig.track_ephemeral_env(env_name,
|
|
294
|
+
"card_number" => card_number,
|
|
295
|
+
"project" => project_key,
|
|
296
|
+
"parent_env" => parent_env,
|
|
297
|
+
"worktree" => worktree_path)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def create_and_deploy_ephemeral_env(worktree_path:, env_name:, parent_env:, card_number:, project_key:)
|
|
301
|
+
success = BeltEnvironment.create_environment(
|
|
302
|
+
worktree: worktree_path, env_name: env_name, parent_env: parent_env
|
|
303
|
+
)
|
|
304
|
+
unless success
|
|
305
|
+
LOG.error "[EphemeralEnv] Failed to create environment '#{env_name}'"
|
|
306
|
+
return
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
BeltConfig.track_ephemeral_env(env_name,
|
|
310
|
+
"card_number" => card_number,
|
|
311
|
+
"project" => project_key,
|
|
312
|
+
"parent_env" => parent_env,
|
|
313
|
+
"worktree" => worktree_path)
|
|
314
|
+
LOG.info "[EphemeralEnv] Configured #{env_name} in worktree, deploying in background"
|
|
315
|
+
|
|
316
|
+
Thread.new do
|
|
317
|
+
frontend_only = BeltEnvironment.frontend_only_changes?(worktree: worktree_path)
|
|
318
|
+
BeltEnvironment.deploy(worktree: worktree_path, env_name: env_name, frontend_only: frontend_only)
|
|
319
|
+
rescue StandardError => e
|
|
320
|
+
LOG.error "[EphemeralEnv] Error deploying '#{env_name}': #{e.message}"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -453,6 +453,10 @@ def handle_existing_card_comment(ctx)
|
|
|
453
453
|
work_dir = worktree && File.directory?(worktree) ? worktree : ctx.project_config["repo_path"]
|
|
454
454
|
card_key = "card-#{card_number || ctx.card_internal_id}"
|
|
455
455
|
|
|
456
|
+
# Fizzy has no tag-added webhook. On comment, fetch live tags and configure
|
|
457
|
+
# the Belt env in this worktree before dispatching (or queueing) the agent.
|
|
458
|
+
ensure_ephemeral_env_for_comment(ctx, card_number, worktree)
|
|
459
|
+
|
|
456
460
|
# Session management (wait, supersede, or queue)
|
|
457
461
|
queued = handle_session_conflict(ctx, card_key, card_number, work_dir)
|
|
458
462
|
return queued if queued
|
|
@@ -491,6 +495,8 @@ def handle_new_mention(ctx)
|
|
|
491
495
|
react_to_comment(card_number, ctx.comment_id, ctx.project_config, ctx.agent_name, "👀")
|
|
492
496
|
|
|
493
497
|
worktree_path, branch = setup_new_mention_worktree(ctx, card_number, card_title)
|
|
498
|
+
ensure_ephemeral_env_for_comment(ctx, card_number, worktree_path)
|
|
499
|
+
|
|
494
500
|
dispatch_new_mention(ctx, card_key: card_key, card_number: card_number,
|
|
495
501
|
card_title: card_title, branch: branch, worktree_path: worktree_path)
|
|
496
502
|
end
|
|
@@ -33,6 +33,22 @@ module Brainiac
|
|
|
33
33
|
fizzy_env_for(AI_AGENT_NAME)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Resolve GitHub App token for an agent so gh CLI runs as their bot identity.
|
|
37
|
+
# Uses brainiac-github's AppClient if available.
|
|
38
|
+
def resolve_github_agent_env(agent_name, github_repo)
|
|
39
|
+
return {} unless github_repo
|
|
40
|
+
|
|
41
|
+
if defined?(Brainiac::Plugins::Github::AppClient)
|
|
42
|
+
repo_owner = github_repo.split("/").first
|
|
43
|
+
token = Brainiac::Plugins::Github::AppClient.installation_token_for(agent_name, repo_owner: repo_owner)
|
|
44
|
+
return { "GH_TOKEN" => token } if token
|
|
45
|
+
end
|
|
46
|
+
{}
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
LOG.warn "[Fizzy] Could not resolve GitHub token for #{agent_name}: #{e.message}" if defined?(LOG)
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
|
|
36
52
|
def prefetch_card_context(card_number, repo_path:, agent_name: nil)
|
|
37
53
|
env = fizzy_env_for(agent_name || AI_AGENT_NAME)
|
|
38
54
|
card_details = fetch_card_details(card_number, repo_path: repo_path, env: env)
|
|
@@ -44,6 +60,37 @@ module Brainiac
|
|
|
44
60
|
context
|
|
45
61
|
end
|
|
46
62
|
|
|
63
|
+
# Normalize Fizzy tags from webhook hashes ({ "name" => "deploy" })
|
|
64
|
+
# or API strings ("deploy") into a lowercase name list.
|
|
65
|
+
def tag_names(tags)
|
|
66
|
+
Array(tags).filter_map do |tag|
|
|
67
|
+
name = tag.is_a?(Hash) ? (tag["name"] || tag[:name]) : tag
|
|
68
|
+
normalized = name.to_s.downcase
|
|
69
|
+
normalized unless normalized.empty?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def card_has_tag?(tags, name)
|
|
74
|
+
tag_names(tags).include?(name.to_s.downcase)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Live tags from `fizzy card show`. Returns nil on failure so callers
|
|
78
|
+
# can fall back to webhook payload tags. Fizzy has no tag-added webhook;
|
|
79
|
+
# comment payloads may omit or stale-cache tags, so this is the source of truth.
|
|
80
|
+
def fetch_card_tags(card_number, repo_path:, env: nil)
|
|
81
|
+
return nil unless card_number && repo_path
|
|
82
|
+
|
|
83
|
+
env ||= default_fizzy_env
|
|
84
|
+
output = run_cmd("fizzy", "card", "show", card_number.to_s, chdir: repo_path, env: env)
|
|
85
|
+
card = JSON.parse(output)["data"]
|
|
86
|
+
return nil unless card
|
|
87
|
+
|
|
88
|
+
Array(card["tags"])
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
LOG.warn "[Fizzy] Could not fetch tags for card ##{card_number}: #{e.message}" if defined?(LOG)
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
47
94
|
def fetch_card_details(card_number, repo_path:, env:)
|
|
48
95
|
output = run_cmd("fizzy", "card", "show", card_number.to_s, chdir: repo_path, env: env)
|
|
49
96
|
card = JSON.parse(output)["data"]
|
|
@@ -167,17 +167,63 @@ module Brainiac
|
|
|
167
167
|
rescue StandardError => e
|
|
168
168
|
LOG.error "[Fizzy] Error in pr_merged hook: #{e.message}" if defined?(LOG)
|
|
169
169
|
end
|
|
170
|
+
|
|
171
|
+
# Also listen for epic branch merges (from brainiac-basecamp)
|
|
172
|
+
# Move card to UAT when merged to any tracked branch
|
|
173
|
+
Brainiac.on(:pr_merged_to_branch) do |ctx|
|
|
174
|
+
card_number = ctx[:card_number]
|
|
175
|
+
next unless card_number
|
|
176
|
+
|
|
177
|
+
# Find the work item to get card info
|
|
178
|
+
branch = ctx[:branch]
|
|
179
|
+
work_item = find_work_item_by_branch(branch) if respond_to?(:find_work_item_by_branch, true)
|
|
180
|
+
work_item ||= Object.respond_to?(:find_work_item_by_branch, true) ? Object.send(:find_work_item_by_branch, branch) : nil
|
|
181
|
+
|
|
182
|
+
next unless work_item
|
|
183
|
+
|
|
184
|
+
_internal_id, card_info = work_item
|
|
185
|
+
card_agent = card_info["agent"]
|
|
186
|
+
board_key = card_info.dig("sources", "fizzy", "board_key")
|
|
187
|
+
|
|
188
|
+
# Load project config
|
|
189
|
+
project_key = card_info.dig("sources", "fizzy", "project") || card_info["project"]
|
|
190
|
+
projects_file = File.join(ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac")), "projects.json")
|
|
191
|
+
projects = File.exist?(projects_file) ? JSON.parse(File.read(projects_file)) : {}
|
|
192
|
+
project_config = projects[project_key] || {}
|
|
193
|
+
repo_path = project_config["repo_path"]
|
|
194
|
+
|
|
195
|
+
next unless repo_path
|
|
196
|
+
|
|
197
|
+
# Move card to UAT (same as main merge)
|
|
198
|
+
LOG.info "[Fizzy] PR merged to #{ctx[:base_branch]} for card ##{card_number} — moving to UAT" if defined?(LOG)
|
|
199
|
+
Helpers.move_card_to_column(card_number, "uat",
|
|
200
|
+
project_config: project_config,
|
|
201
|
+
agent_name: card_agent,
|
|
202
|
+
board_key: board_key)
|
|
203
|
+
record_self_move(card_number)
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
LOG.error "[Fizzy] Error in pr_merged_to_branch hook: #{e.message}" if defined?(LOG)
|
|
206
|
+
end
|
|
170
207
|
end
|
|
171
208
|
|
|
172
209
|
# PR review received — post status comment on card
|
|
210
|
+
# Only for changes_requested reviews that need action, not for approvals or gate reviews
|
|
173
211
|
def register_pr_review_received
|
|
174
212
|
Brainiac.on(:pr_review_received) do |ctx|
|
|
175
213
|
card_number = ctx[:card_number]
|
|
176
214
|
next unless card_number
|
|
177
215
|
|
|
216
|
+
# Only post status for changes_requested — approvals don't need a Fizzy comment
|
|
217
|
+
review_state = ctx[:review_state]
|
|
218
|
+
next unless review_state == "changes_requested"
|
|
219
|
+
|
|
220
|
+
# Skip if this is a gate agent review (handled by brainiac-basecamp)
|
|
221
|
+
reviewer = ctx[:reviewer]
|
|
222
|
+
next if reviewer&.match?(/-(brainiac|bot)\b/i)
|
|
223
|
+
|
|
178
224
|
Thread.new do
|
|
179
225
|
env = Helpers.fizzy_env_for(ctx[:agent_name])
|
|
180
|
-
status_comment = "<p>🔄 Code review received from @#{
|
|
226
|
+
status_comment = "<p>🔄 Code review received from @#{reviewer}. Updates in progress...</p>"
|
|
181
227
|
run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", status_comment,
|
|
182
228
|
chdir: ctx[:repo_path], env: env)
|
|
183
229
|
rescue StandardError => e
|