brainiac-fizzy 0.0.28 → 0.0.30

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: e5d55859eafd2aeccf63a2083999340a1ce4a77c01b81a8297adf136b0e553b3
4
- data.tar.gz: b1dbff19165dec2cf391f4c258ae2a207457cdc53088ee44062ffda050530a48
3
+ metadata.gz: 8e54fe3a10cd6e39be3f52862b1625e2532be592e5480c11f685423910a75b91
4
+ data.tar.gz: 77cc0b0ce8a518bde93484bf8513f538f9a26f4412479556d80f83de3ae5c545
5
5
  SHA512:
6
- metadata.gz: 9438b9143636ab612da09a19be5b672ada809095317ea7ce72d9045acbebe6601d5485ba4bfd234b85edcdb54a6d676edd330a74b4241cc85c0605d590fe523d
7
- data.tar.gz: 93fbe26b2a546b1b72e1025f7af334ac6539efe0c4144338bd6d2929b336e2ed21752cc8a1d4ef2949d380cc72635e945e0dad819afe043570c483ddc889d2d7
6
+ metadata.gz: 71c5abb12093956521a3ebdf78566d416d1c3ed33293d6c402a5236462b524042af9a259a3cda7fbf1930c80b93e7be68462952aed0f61393a1eb84b055a851a
7
+ data.tar.gz: 1ecc72a231ccef4c9fadbc279fe561628fda23151aad9b8b64afd65cd151d7ec612d95c25daa008958176b0e922b582602689f6c0754a0474643a4216131055c
data/README.md CHANGED
@@ -37,6 +37,8 @@ Fizzy configuration lives in `~/.brainiac/fizzy.json` (same as before):
37
37
  { "id": "user-id-1", "name": "Andy", "human": true },
38
38
  { "id": "agent-id-1", "name": "Galen", "human": false }
39
39
  ],
40
+ "uat_agent": false,
41
+ "uat_agent_tag": "uat",
40
42
  "boards": {
41
43
  "development": {
42
44
  "board_id": "your-board-id",
@@ -51,6 +53,25 @@ Fizzy configuration lives in `~/.brainiac/fizzy.json` (same as before):
51
53
  }
52
54
  ```
53
55
 
56
+ ### UAT agent (`uat_agent`)
57
+
58
+ When a PR is merged and its card moves to the UAT column, Brainiac can dispatch
59
+ an LLM agent to write manual testing steps as a card comment. This spends tokens
60
+ on every merge, so it's **disabled by default**. Set `"uat_agent": true` at the
61
+ top level of `fizzy.json` to opt back in globally. The card still moves to UAT
62
+ and gets the PR-merged comment either way — only the agent dispatch is gated.
63
+
64
+ **Per-card opt-in:** Even with `uat_agent` off globally, you can trigger the UAT
65
+ agent for a single card by adding the `uat` tag to it. When the card's PR merges,
66
+ Brainiac fetches the card's live tags and dispatches the UAT agent if the tag is
67
+ present. Customize the tag name with `"uat_agent_tag": "your-tag"`, or set it to
68
+ `false`/`null` to disable per-card overrides entirely. This gives you three
69
+ states:
70
+
71
+ - `uat_agent: true` — dispatch on every merge
72
+ - `uat_agent: false` (default) + `uat` tag on a card — dispatch for that card only
73
+ - `uat_agent: false` + no tag — never dispatch (pure bookkeeping, zero tokens)
74
+
54
75
  ## What This Plugin Handles
55
76
 
56
77
  | Event | Action |
@@ -27,6 +27,8 @@ module Brainiac
27
27
  cmd_setup
28
28
  when "board"
29
29
  cmd_board(args)
30
+ when "env"
31
+ cmd_env(args)
30
32
  else
31
33
  print_help
32
34
  end
@@ -126,6 +128,66 @@ module Brainiac
126
128
  end
127
129
  end
128
130
 
131
+ # --- ephemeral env management ---
132
+
133
+ # `brainiac fizzy env setup <card_number>`
134
+ #
135
+ # Manually set up (or refresh tracking for) an ephemeral Belt env for a
136
+ # card that was already worked — no agent, no tokens. Once tracked, PR
137
+ # updates auto-redeploy it via brainiac-github.
138
+ def cmd_env(args)
139
+ sub = args.shift
140
+ case sub
141
+ when "setup", "add"
142
+ env_setup(args)
143
+ else
144
+ puts "Usage: brainiac fizzy env setup <card_number>"
145
+ puts ""
146
+ puts "Commands:"
147
+ puts " setup <card_number> Configure + deploy an ephemeral Belt env for a"
148
+ puts " card that was already worked (no agent dispatch)."
149
+ end
150
+ end
151
+
152
+ def env_setup(args)
153
+ card_number = args.shift
154
+ if card_number.nil? || card_number.strip.empty?
155
+ puts "Usage: brainiac fizzy env setup <card_number>"
156
+ return
157
+ end
158
+
159
+ require "net/http"
160
+ require "uri"
161
+
162
+ server_url = detect_server_url
163
+ uri = URI("#{server_url}/api/fizzy/ephemeral-env/#{card_number}")
164
+
165
+ puts "Setting up ephemeral env for card ##{card_number} (no agent)..."
166
+ begin
167
+ http = Net::HTTP.new(uri.host, uri.port)
168
+ http.read_timeout = 600 # belt deploy can be slow
169
+ response = http.post(uri.path, "")
170
+ data = begin
171
+ JSON.parse(response.body)
172
+ rescue StandardError
173
+ {}
174
+ end
175
+
176
+ if response.is_a?(Net::HTTPSuccess) && data["status"] == "ok"
177
+ puts "✓ Ephemeral env '#{data["env"]}' ready#{" (was already configured)" if data["already_configured"]}"
178
+ puts " Worktree: #{data["worktree"]}" if data["worktree"]
179
+ puts " URL: #{data["url"]}" if data["url"]
180
+ puts ""
181
+ puts "Tracked in ephemeral_envs.json — PR updates will auto-redeploy it."
182
+ else
183
+ puts "✗ Failed: #{data["reason"] || response.message}"
184
+ end
185
+ rescue StandardError => e
186
+ puts "Could not reach server at #{server_url}: #{e.message}"
187
+ puts "Is the server running? Check with: brainiac status"
188
+ end
189
+ end
190
+
129
191
  # --- board setup (split into phases) ---
130
192
 
131
193
  def board_setup
@@ -629,6 +691,7 @@ module Brainiac
629
691
  board list List configured boards and their columns
630
692
  board assign <project> <board_key> Assign a project to a board
631
693
  board columns <board_key> Refresh columns for a board from Fizzy
694
+ env setup <card_number> Set up an ephemeral Belt env for an already-worked card (no agent)
632
695
 
633
696
  Fizzy handles card assignment, comments, @mentions, cross-agent reviews,
634
697
  duplicate detection, and planning mode via webhooks.
@@ -662,11 +725,13 @@ module Brainiac
662
725
  # When called with args (the words typed so far after the plugin name),
663
726
  # returns context-sensitive completions for nested subcommands.
664
727
  def self.completions(args = [])
665
- return %w[board config setup status] if args.empty?
728
+ return %w[board config env setup status] if args.empty?
666
729
 
667
730
  case args[0]
668
731
  when "board"
669
732
  board_completions(args[1..])
733
+ when "env"
734
+ args.length <= 1 ? %w[setup add] : []
670
735
  else
671
736
  []
672
737
  end
@@ -82,6 +82,26 @@ module Brainiac
82
82
  nil
83
83
  end
84
84
 
85
+ # Whether to dispatch an LLM agent to write UAT testing steps when a
86
+ # PR is merged and the card moves to UAT. Disabled by default to save
87
+ # tokens — set "uat_agent": true in ~/.brainiac/fizzy.json to enable
88
+ # globally. Individual cards can still opt in via the UAT tag even when
89
+ # this is false (see #uat_agent_tag).
90
+ def uat_agent_enabled?
91
+ @config.fetch("uat_agent", false) == true
92
+ end
93
+
94
+ # The card tag that opts a single card into UAT agent dispatch even
95
+ # when uat_agent is globally disabled. Defaults to "uat". Set
96
+ # "uat_agent_tag": "some-tag" in fizzy.json to customize, or false/null
97
+ # to disable per-card overrides entirely.
98
+ def uat_agent_tag
99
+ value = @config.fetch("uat_agent_tag", "uat")
100
+ return nil if value == false || value.nil? || value.to_s.strip.empty?
101
+
102
+ value.to_s
103
+ end
104
+
85
105
  def authorized?(payload)
86
106
  creator_id = payload.dig("creator", "id")
87
107
  @authorized_user_ids.include?(creator_id)
@@ -194,8 +194,13 @@ end
194
194
  #
195
195
  # `belt g environment` is synchronous so the worktree is configured before the
196
196
  # agent starts. The actual `belt deploy` runs in the background.
197
+ #
198
+ # When `force: true`, the `deploy` tag requirement is bypassed — the caller is
199
+ # explicitly requesting env setup (e.g. the `brainiac fizzy env setup` CLI or
200
+ # the `/api/fizzy/ephemeral-env/:card` endpoint), so no tag is needed.
197
201
  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)
202
+ repo_path: nil, agent_name: nil, fetch_live_tags: false,
203
+ force: false)
199
204
  unless defined?(BeltConfig) && defined?(BeltEnvironment)
200
205
  LOG.debug "[EphemeralEnv] Belt utilities not available — skipping"
201
206
  return
@@ -216,14 +221,16 @@ def maybe_create_ephemeral_belt_env(worktree_path:, card_number:, project_key:,
216
221
  return
217
222
  end
218
223
 
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
224
+ unless force
225
+ resolved_tags = resolve_ephemeral_env_tags(
226
+ tags, card_number: card_number, repo_path: repo_path || worktree_path,
227
+ agent_name: agent_name, fetch_live_tags: fetch_live_tags
228
+ )
229
+ tag_list = tag_names(resolved_tags)
230
+ unless tag_list.include?("deploy")
231
+ LOG.debug "[EphemeralEnv] Card ##{card_number} tags=#{tag_list.inspect} — no deploy tag, skipping"
232
+ return
233
+ end
227
234
  end
228
235
 
229
236
  parent_env = BeltConfig.parent_env_for(project_key)
@@ -266,6 +273,75 @@ def ensure_ephemeral_env_for_comment(ctx, card_number, worktree)
266
273
  )
267
274
  end
268
275
 
276
+ # Manually set up an ephemeral Belt env for a card that was already worked,
277
+ # without dispatching an agent. Resolves the card's existing worktree from the
278
+ # work item map, configures + deploys the env synchronously, and tracks it in
279
+ # ephemeral_envs.json so PR-update auto-redeploys (brainiac-github) work.
280
+ #
281
+ # Bypasses the `deploy` tag requirement (force: true) — the caller is asking
282
+ # for this explicitly. Returns a result hash suitable for a JSON API response.
283
+ #
284
+ # @param card_number [Integer, String] Fizzy card number
285
+ # @return [Hash] { status:, env:, url:, worktree:, reason: }
286
+ def setup_ephemeral_env_for_card(card_number)
287
+ return { status: "error", reason: "card_number required" } unless card_number
288
+
289
+ unless defined?(BeltConfig) && defined?(BeltEnvironment)
290
+ return { status: "error", reason: "Belt utilities not available" }
291
+ end
292
+
293
+ card_number = card_number.to_s
294
+ entry = work_item_entry_for_card(card_number)
295
+ return { status: "error", reason: "no work item found for card ##{card_number}" } unless entry
296
+
297
+ worktree = entry["worktree"]
298
+ project_key = entry["project"]
299
+ unless worktree && File.directory?(worktree)
300
+ return { status: "error", reason: "worktree missing for card ##{card_number} (#{worktree.inspect})" }
301
+ end
302
+
303
+ env_name = BeltConfig.ephemeral_env_for_card(card_number)
304
+ already = BeltEnvironment.environment_configured?(worktree: worktree, env_name: env_name)
305
+
306
+ maybe_create_ephemeral_belt_env(
307
+ worktree_path: worktree, card_number: card_number,
308
+ project_key: project_key, force: true
309
+ )
310
+
311
+ # After setup, resolve the public URL the same way deployment lookups do.
312
+ url = defined?(deployment_url_for_card) ? (deployment_url_for_card(card_number) || {})[:url] : nil
313
+
314
+ {
315
+ status: "ok",
316
+ env: env_name,
317
+ url: url,
318
+ worktree: worktree,
319
+ already_configured: already
320
+ }
321
+ rescue StandardError => e
322
+ LOG.error "[EphemeralEnv] Manual setup failed for card ##{card_number}: #{e.message}"
323
+ { status: "error", reason: e.message }
324
+ end
325
+
326
+ # Resolve the work item entry for a Fizzy card number from work_items.json.
327
+ # Returns a flattened hash with worktree/branch/project or nil.
328
+ def work_item_entry_for_card(card_number)
329
+ map = load_work_item_map
330
+ match = map.values.find do |entry|
331
+ entry.dig("sources", "fizzy", "card_number").to_s == card_number.to_s
332
+ end
333
+ return nil unless match
334
+
335
+ {
336
+ "worktree" => match["worktree"],
337
+ "branch" => match["branch"],
338
+ "project" => match["project"]
339
+ }
340
+ rescue StandardError => e
341
+ LOG.error "[EphemeralEnv] Could not load work item for card ##{card_number}: #{e.message}" if defined?(LOG)
342
+ nil
343
+ end
344
+
269
345
  def belt_app_for_ephemeral?(worktree_path)
270
346
  return BeltEnvironment.belt_app?(worktree_path) if defined?(BeltEnvironment) && BeltEnvironment.respond_to?(:belt_app?)
271
347
 
@@ -134,36 +134,7 @@ module Brainiac
134
134
  # PR merged — post comment on Fizzy card, move to UAT, dispatch UAT agent
135
135
  def register_pr_merged
136
136
  Brainiac.on(:pr_merged) do |ctx|
137
- card_number = ctx[:card_number]
138
- next unless card_number
139
-
140
- card_info = ctx[:card_info]
141
- card_agent = card_info["agent"]
142
- env = Helpers.fizzy_env_for(card_agent)
143
- repo_path = ctx[:repo_path]
144
- board_key = card_info.dig("sources", "fizzy", "board_key") || Config.board_key_for_project(ctx[:project_config])
145
-
146
- # Post PR link comment
147
- pr_url = ctx[:pr_url]
148
- pr_title = ctx[:pr_title]
149
- branch = ctx[:branch]
150
- comment_body = "<p>PR merged into main: <a href=\"#{pr_url}\">#{pr_title}</a></p>" \
151
- "<p>Branch: <code>#{branch}</code></p>"
152
- run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", comment_body,
153
- chdir: repo_path, env: env)
154
-
155
- # Move card to UAT
156
- Helpers.move_card_to_column(card_number, "uat",
157
- project_config: ctx[:project_config],
158
- agent_name: card_agent,
159
- board_key: board_key)
160
- record_self_move(card_number)
161
-
162
- # Clear deployment tracking
163
- clear_deployment_for_card(card_number) if respond_to?(:clear_deployment_for_card)
164
-
165
- # Dispatch UAT agent
166
- dispatch_fizzy_uat_agent(ctx)
137
+ handle_pr_merged(ctx)
167
138
  rescue StandardError => e
168
139
  LOG.error "[Fizzy] Error in pr_merged hook: #{e.message}" if defined?(LOG)
169
140
  end
@@ -292,6 +263,75 @@ module Brainiac
292
263
 
293
264
  # --- Private helpers ---
294
265
 
266
+ # Body of the :pr_merged hook. Posts the PR link comment, moves the
267
+ # card to UAT, clears deployment tracking, and dispatches the UAT
268
+ # agent when enabled. Extracted from the hook block to keep the
269
+ # registration method within complexity limits.
270
+ def handle_pr_merged(ctx)
271
+ card_number = ctx[:card_number]
272
+ return unless card_number
273
+
274
+ card_info = ctx[:card_info]
275
+ card_agent = card_info["agent"]
276
+ env = Helpers.fizzy_env_for(card_agent)
277
+ repo_path = ctx[:repo_path]
278
+ board_key = card_info.dig("sources", "fizzy", "board_key") || Config.board_key_for_project(ctx[:project_config])
279
+
280
+ # Post PR link comment
281
+ comment_body = "<p>PR merged into main: <a href=\"#{ctx[:pr_url]}\">#{ctx[:pr_title]}</a></p>" \
282
+ "<p>Branch: <code>#{ctx[:branch]}</code></p>"
283
+ run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", comment_body,
284
+ chdir: repo_path, env: env)
285
+
286
+ # Move card to UAT
287
+ Helpers.move_card_to_column(card_number, "uat",
288
+ project_config: ctx[:project_config],
289
+ agent_name: card_agent,
290
+ board_key: board_key)
291
+ record_self_move(card_number)
292
+
293
+ # Clear deployment tracking
294
+ clear_deployment_for_card(card_number) if respond_to?(:clear_deployment_for_card)
295
+
296
+ # Dispatch UAT agent — off by default to save tokens. Globally
297
+ # enabled via "uat_agent": true, or per-card via the UAT tag
298
+ # (default "uat") even when the global flag is off.
299
+ if uat_agent_dispatch?(card_number, repo_path: repo_path, env: env)
300
+ dispatch_fizzy_uat_agent(ctx)
301
+ else
302
+ log_uat_dispatch_skipped(card_number)
303
+ end
304
+ end
305
+
306
+ # Log that UAT dispatch was skipped for a card (global flag off, no tag).
307
+ def log_uat_dispatch_skipped(card_number)
308
+ return unless defined?(LOG)
309
+
310
+ LOG.info "[Fizzy] UAT agent disabled (set \"uat_agent\": true in fizzy.json, or add the " \
311
+ "\"#{Config.uat_agent_tag}\" tag to this card) — skipping dispatch for card ##{card_number}"
312
+ end
313
+
314
+ # Decide whether to dispatch the UAT agent for a merged card.
315
+ # True when globally enabled, OR when the card carries the configured
316
+ # UAT tag (per-card opt-in). Card tags aren't in the pr_merged ctx, so
317
+ # we fetch them live — only when the global flag is off, to avoid an
318
+ # extra API call when it's already enabled.
319
+ def uat_agent_dispatch?(card_number, repo_path:, env: nil)
320
+ return true if Config.uat_agent_enabled?
321
+
322
+ tag = Config.uat_agent_tag
323
+ return false unless tag && card_number && repo_path
324
+
325
+ tags = Helpers.fetch_card_tags(card_number, repo_path: repo_path, env: env)
326
+ return false unless tags
327
+
328
+ has_tag = Helpers.card_has_tag?(tags, tag)
329
+ if has_tag && defined?(LOG)
330
+ LOG.info "[Fizzy] Card ##{card_number} has \"#{tag}\" tag — dispatching UAT agent despite global flag being off"
331
+ end
332
+ has_tag
333
+ end
334
+
295
335
  def dispatch_fizzy_uat_agent(ctx)
296
336
  card_number = ctx[:card_number]
297
337
  card_info = ctx[:card_info]
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Fizzy
6
- VERSION = "0.0.28"
6
+ VERSION = "0.0.30"
7
7
  end
8
8
  end
9
9
  end
@@ -103,8 +103,7 @@ module Brainiac
103
103
  end
104
104
 
105
105
  # Card index API (duplicate detection)
106
- app.get "/api/card-index" do
107
- content_type :json
106
+ app.get "/api/card-index" do content_type :json
108
107
  halt 404, { error: "Card index not available" }.to_json unless defined?(CARD_INDEX)
109
108
 
110
109
  query = params["q"]
@@ -116,6 +115,21 @@ module Brainiac
116
115
  end
117
116
  end
118
117
 
118
+ # Manually set up an ephemeral Belt env for a card that was already
119
+ # worked — no agent dispatch, no tokens. Configures + deploys the env
120
+ # in the card's existing worktree and tracks it so PR-update
121
+ # auto-redeploys keep it fresh.
122
+ app.post "/api/fizzy/ephemeral-env/:card" do
123
+ content_type :json
124
+ card_number = params["card"]
125
+ result = setup_ephemeral_env_for_card(card_number)
126
+ if result[:status] == "ok"
127
+ result.to_json
128
+ else
129
+ halt 422, result.to_json
130
+ end
131
+ end
132
+
119
133
  # Deployment API routes (if deployments are configured)
120
134
  return unless defined?(DEPLOYMENTS_CONFIG)
121
135
 
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.28
4
+ version: 0.0.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis