brainiac-fizzy 0.0.25 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d280084c82934cffbe0868040f944eab1f244ccc10563db9b7bd8a338f86d175
4
- data.tar.gz: 47343a66a5232ea0bdc3d5e0ad8244692e7b4a8ee22efc91656f0e06e6a00c8d
3
+ metadata.gz: 7b9d6e5b3e6e6ea9306dc6fd9ce800dffc01e46f99b05f2861e0acf096793fe5
4
+ data.tar.gz: ec15a0b6f7a498de7662b8bd662fb519ae45f8a14d66e5912cf6dd540cc3726b
5
5
  SHA512:
6
- metadata.gz: f6c5ddfde5c2eaf03ad0ce86bd996f6d28d1662be8e486015b16edf6c9a158d91638db521ea6f8d269b795d5f64c590e22cc0905c5fcfda7cb9a68c7494ddfd2
7
- data.tar.gz: 3371c4fd11abbcdbee4ca7d5b520b1bba0180ad1c61e4172ca77cc95820cef496fc24380a71c97eb0bbe9ce4e5e48fa6934224ad605f393a5db8afb2b6b04f25
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
- def self.completions
662
- %w[config status setup]
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,18 @@ 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
+
28
40
  def resolve_github_agent_env(agent_name, github_repo)
29
41
  Brainiac::Plugins::Fizzy::Helpers.resolve_github_agent_env(agent_name, github_repo)
30
42
  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,
@@ -183,3 +187,136 @@ def dispatch_assigned_card(card_number:, card_internal_id:, title:, tags:, branc
183
187
 
184
188
  [200, { status: "processed", card: card_number, branch: branch, project: project_key, agent: agent_name }.to_json]
185
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
@@ -60,6 +60,37 @@ module Brainiac
60
60
  context
61
61
  end
62
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
+
63
94
  def fetch_card_details(card_number, repo_path:, env:)
64
95
  output = run_cmd("fizzy", "card", "show", card_number.to_s, chdir: repo_path, env: env)
65
96
  card = JSON.parse(output)["data"]
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Fizzy
6
- VERSION = "0.0.25"
6
+ VERSION = "0.0.26"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-fizzy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.25
4
+ version: 0.0.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis