brainiac-basecamp 0.0.19 → 0.0.21

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: e7d5fd8ee1c180d8083b77c3324d138678276d8bbc471dfa48c51d43ae0cca06
4
- data.tar.gz: ec08e6a76392e28d5ef6b6a88adac7712bb60887eca9643d02227f556c7d472a
3
+ metadata.gz: 25a59b3d406964ab51bf4c1d34e63f01cf87368afcf79a1ecd8ce95b304b9ec6
4
+ data.tar.gz: d7e7f94f7b9b6e6a7c1d4480fcf46c6330a9d3928c4afd723c994703e149c70a
5
5
  SHA512:
6
- metadata.gz: c82d88fd5a6cc8cf77330773b7eff552a3bfb7db291627ab0aef5dc3e8e178cee829389a0f72abd1938e4610c04878f3688e0963091a5361f6ae9dcea9f63b4c
7
- data.tar.gz: 7ecb2081edefc0da28093bb5eb2b3cd26da316926349093bf51a36c7308073b5387279d3723a0a814c05f3a79dd7a4b17df07c7f479cc132f0fbeb75d5b64f88
6
+ metadata.gz: a9529e9564869624ac8712d157fbcafae3693018a6106c7f1ea99797fab5fed3e22d967b4c9f5fb4e8319c722d4d1afebb27732c152e9fca4c7b68626ef36ea6
7
+ data.tar.gz: 6cf76c4f8a5b3eaef3f571983f1130a6a79de6a68a22d0a8b08d8fb3d6cb245133e76bd396dcfc80f20ee2be0271757a1e3337ef5797331cc6c984ede6629131
data/README.md CHANGED
@@ -201,6 +201,16 @@ brainiac restart
201
201
  { "agent": "GLaDOS", "role": "test-engineer" },
202
202
  { "agent": "Threepio", "role": "code-reviewer" }
203
203
  ],
204
+ "deploy": {
205
+ "enabled": true,
206
+ "trigger": "on_final_pr",
207
+ "default_env": "dev",
208
+ "project_envs": {
209
+ "stowzilla": "dev",
210
+ "posh-nosh": "dev02"
211
+ },
212
+ "command": "belt deploy {env} --auto"
213
+ },
204
214
  "notifications": {
205
215
  "channel": "discord",
206
216
  "target": "1423854179880927274",
@@ -212,6 +222,66 @@ brainiac restart
212
222
  }
213
223
  ```
214
224
 
225
+ ## Epic Deployment
226
+
227
+ Deploy epics to test environments before merging the final PR to main.
228
+
229
+ ### Per-Epic Deploy Environment
230
+
231
+ Specify the deploy environment in the epic title or description:
232
+
233
+ ```
234
+ Todolist: "Epic: Build Authentication System [deploy:dev02]"
235
+ ```
236
+
237
+ Or in the todolist description:
238
+
239
+ ```
240
+ deploy:dev02
241
+ ```
242
+
243
+ The title syntax takes precedence over description.
244
+
245
+ ### Manual Deploy
246
+
247
+ ```bash
248
+ brainiac basecamp deploy <epic-id> [env]
249
+ ```
250
+
251
+ Environment resolution (in order):
252
+ 1. Explicit env argument
253
+ 2. `[deploy:env]` in epic title
254
+ 3. `deploy:env` in epic description
255
+ 4. `deploy.project_envs.<project>` in config (per-project default)
256
+ 5. `deploy.default_env` in config (global default)
257
+
258
+ ### Automated Deploy
259
+
260
+ Configure automated deployment in `~/.brainiac/basecamp.json`:
261
+
262
+ ```json
263
+ {
264
+ "deploy": {
265
+ "enabled": true,
266
+ "trigger": "on_final_pr",
267
+ "default_env": "dev",
268
+ "project_envs": {
269
+ "stowzilla": "dev",
270
+ "posh-nosh": "dev02"
271
+ },
272
+ "command": "belt deploy {env} --auto"
273
+ }
274
+ }
275
+ ```
276
+
277
+ | Option | Description |
278
+ |--------|-------------|
279
+ | `enabled` | Enable/disable automated deploys |
280
+ | `trigger` | When to deploy: `manual`, `on_final_pr` |
281
+ | `default_env` | Global fallback environment |
282
+ | `project_envs` | Per-project default environments (e.g., `{"stowzilla": "dev"}`) |
283
+ | `command` | Deploy command template (`{env}` is replaced) |
284
+
215
285
  ## CLI Commands
216
286
 
217
287
  ```bash
@@ -220,6 +290,7 @@ brainiac basecamp config # Show config
220
290
  brainiac basecamp status # Plugin health check
221
291
  brainiac basecamp epics # List active epics
222
292
  brainiac basecamp epics --all # Include completed
293
+ brainiac basecamp deploy <epic-id> [env] # Deploy epic to environment
223
294
  brainiac basecamp bot add <name> <id> <agent> # Add bot account
224
295
  brainiac basecamp bot list # List bot accounts
225
296
  brainiac basecamp projects map <key> <bc-id> # Map project
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "json"
4
4
  require "open3"
5
+ require_relative "config"
6
+ require_relative "epic"
5
7
 
6
8
  module Brainiac
7
9
  module Plugins
@@ -23,6 +25,8 @@ module Brainiac
23
25
  cmd_status
24
26
  when "epics"
25
27
  cmd_epics(args)
28
+ when "deploy"
29
+ cmd_deploy(args)
26
30
  when "link"
27
31
  cmd_link(args)
28
32
  when "bot"
@@ -163,14 +167,14 @@ module Brainiac
163
167
  data = JSON.parse(File.read(epics_file))
164
168
  epics = data["epics"] || []
165
169
 
166
- if args.first == "--all"
170
+ if args.include?("--all")
167
171
  display_epics = epics
168
172
  else
169
173
  display_epics = epics.select { |e| e["status"] == "active" }
170
174
  end
171
175
 
172
176
  if display_epics.empty?
173
- puts "No #{args.first == '--all' ? '' : 'active '}epics."
177
+ puts "No #{args.include?('--all') ? '' : 'active '}epics."
174
178
  return
175
179
  end
176
180
 
@@ -181,11 +185,23 @@ module Brainiac
181
185
  total = tasks.size
182
186
 
183
187
  status_icon = epic["status"] == "active" ? "🚀" : "✅"
184
- puts "#{status_icon} #{epic['title']}"
188
+
189
+ # Show deploy env if configured
190
+ deploy_env = Basecamp::Epic.extract_deploy_env(epic["title"], epic["description"])
191
+ deploy_display = deploy_env ? " [deploy:#{deploy_env}]" : ""
192
+
193
+ puts "#{status_icon} #{epic['title']}#{deploy_display}"
185
194
  puts " Todolist: #{epic['basecamp_todolist_id']} | Agent: #{epic['agent']}"
186
195
  puts " Tasks: #{complete}/#{total} complete, #{in_flight} in-flight"
187
196
  puts " Started: #{epic['started_at']}"
188
197
 
198
+ # Show last deploy if present
199
+ if epic["last_deploy"]
200
+ ld = epic["last_deploy"]
201
+ deploy_status = ld["status"] == "success" ? "✅" : "❌"
202
+ puts " Last deploy: #{deploy_status} #{ld['env']} @ #{ld['at']}"
203
+ end
204
+
189
205
  # Show per-task detail if verbose or few tasks
190
206
  if args.include?("--verbose") || args.include?("-v")
191
207
  tasks.each do |t|
@@ -203,6 +219,98 @@ module Brainiac
203
219
  end
204
220
  end
205
221
 
222
+ def cmd_deploy(args)
223
+ epic_id = args.shift
224
+ env_override = args.shift
225
+
226
+ unless epic_id
227
+ puts "Usage: brainiac basecamp deploy <epic-id> [env]"
228
+ puts ""
229
+ puts "Deploy an epic's worktree to the specified environment."
230
+ puts ""
231
+ puts "Arguments:"
232
+ puts " <epic-id> Basecamp todolist ID for the epic"
233
+ puts " [env] Environment to deploy to (overrides epic's [deploy:env])"
234
+ puts ""
235
+ puts "Environment resolution (in order):"
236
+ puts " 1. Explicit env argument"
237
+ puts " 2. [deploy:env] in epic title"
238
+ puts " 3. deploy:env in epic description"
239
+ puts " 4. deploy.default_env in config"
240
+ puts ""
241
+ puts "Active epics:"
242
+ list_epics_for_deploy
243
+ return
244
+ end
245
+
246
+ # Load epic
247
+ epics = load_epics
248
+ epic = epics.find { |e| e["basecamp_todolist_id"] == epic_id.to_s }
249
+
250
+ unless epic
251
+ puts "❌ No epic found with todolist ID #{epic_id}"
252
+ puts ""
253
+ puts "Active epics:"
254
+ list_epics_for_deploy
255
+ return
256
+ end
257
+
258
+ # Resolve deploy environment
259
+ deploy_env = resolve_deploy_env(epic, env_override)
260
+
261
+ unless deploy_env
262
+ puts "❌ No deploy environment specified."
263
+ puts ""
264
+ puts "Either:"
265
+ puts " 1. Pass env as argument: brainiac basecamp deploy #{epic_id} dev02"
266
+ puts " 2. Add [deploy:env] to epic title: \"Epic: My Feature [deploy:dev02]\""
267
+ puts " 3. Add deploy:env to epic description"
268
+ puts " 4. Set deploy.project_envs.<project> in ~/.brainiac/basecamp.json"
269
+ puts " 5. Set deploy.default_env in ~/.brainiac/basecamp.json"
270
+ return
271
+ end
272
+
273
+ # Find worktree
274
+ worktree_path = find_epic_worktree(epic)
275
+
276
+ unless worktree_path
277
+ puts "❌ No worktree found for epic '#{epic['title']}'"
278
+ puts " Epic branch mode may not be enabled, or no tasks have been dispatched."
279
+ return
280
+ end
281
+
282
+ puts "🚀 Deploying epic '#{epic['title']}'"
283
+ puts " Environment: #{deploy_env}"
284
+ puts " Worktree: #{worktree_path}"
285
+ puts ""
286
+
287
+ # Run deploy
288
+ success = run_deploy(worktree_path, deploy_env)
289
+
290
+ puts ""
291
+
292
+ if success
293
+ puts "✅ Deploy completed successfully"
294
+
295
+ # Update epic state
296
+ epic["last_deploy"] = {
297
+ "env" => deploy_env,
298
+ "at" => Time.now.iso8601,
299
+ "status" => "success"
300
+ }
301
+ else
302
+ puts "❌ Deploy failed"
303
+
304
+ epic["last_deploy"] = {
305
+ "env" => deploy_env,
306
+ "at" => Time.now.iso8601,
307
+ "status" => "failed"
308
+ }
309
+ end
310
+
311
+ save_epics(epics)
312
+ end
313
+
206
314
  def cmd_link(args)
207
315
  fizzy_card = args.shift
208
316
  basecamp_url = args.shift
@@ -517,7 +625,8 @@ module Brainiac
517
625
  setup Interactive setup guide
518
626
  config Show current config
519
627
  status Check plugin status
520
- epics [--all] List active epics (--all for completed too)
628
+ epics [--all] [-v] List active epics (--all for completed too)
629
+ deploy <epic-id> [env] Deploy epic worktree to environment
521
630
  link <card> <url> Link a Fizzy card to a Basecamp todo
522
631
  bot add <agent-name> Add bot (auto-resolves person_id from Basecamp)
523
632
  bot add <name> <person-id> <agent> Add bot with explicit person_id
@@ -542,6 +651,13 @@ module Brainiac
542
651
  on_complete — Advance to next task as soon as agent finishes (default)
543
652
  on_pr_merge — Wait for PR to be merged before advancing (review gate)
544
653
 
654
+ Deploy environment resolution (in order):
655
+ 1. Explicit env argument: brainiac basecamp deploy 12345 dev02
656
+ 2. [deploy:env] in epic title: "Epic: My Feature [deploy:dev02]"
657
+ 3. deploy:env in epic description
658
+ 4. deploy.project_envs.<project> in config (per-project default)
659
+ 5. deploy.default_env in config (global default)
660
+
545
661
  Task statuses (for reset --to):
546
662
  pending — Not yet started, waiting for dependencies
547
663
  in_flight — Agent is actively working on it
@@ -873,6 +989,91 @@ module Brainiac
873
989
  }))
874
990
  end
875
991
 
992
+ # --- Deploy helpers ---
993
+
994
+ def list_epics_for_deploy
995
+ epics = load_epics.select { |e| e["status"] == "active" }
996
+ if epics.empty?
997
+ puts " (no active epics)"
998
+ return
999
+ end
1000
+
1001
+ epics.each do |epic|
1002
+ deploy_env = resolve_deploy_env(epic, nil)
1003
+ env_display = deploy_env ? "[deploy:#{deploy_env}]" : "(no env)"
1004
+ puts " #{epic['basecamp_todolist_id']} — #{epic['title']} #{env_display}"
1005
+ end
1006
+ end
1007
+
1008
+ def resolve_deploy_env(epic, override)
1009
+ # 1. Explicit override
1010
+ return override if override && !override.empty?
1011
+
1012
+ # 2. [deploy:env] in title or deploy:env in description
1013
+ env = Basecamp::Epic.extract_deploy_env(epic["title"], epic["description"])
1014
+ return env if env
1015
+
1016
+ # 3. Per-project default from config
1017
+ # Prefer the project from tasks (set from Fizzy card tags) over basecamp mapping
1018
+ # since multiple brainiac projects can share the same basecamp project.
1019
+ project_key = (epic["tasks"] || []).filter_map { |t| t["project"] }.first
1020
+ project_key ||= Config.brainiac_project_for(epic["basecamp_project_id"])
1021
+ project_env = Config.deploy.dig("project_envs", project_key) if project_key
1022
+ return project_env if project_env
1023
+
1024
+ # 4. Global default
1025
+ Config.deploy["default_env"]
1026
+ end
1027
+
1028
+ def find_epic_worktree(epic)
1029
+ # Epic branches are stored per-project in epic["epic_branches"]
1030
+ # Find any worktree that has the epic branch checked out
1031
+ epic_branches = epic["epic_branches"] || {}
1032
+ return nil if epic_branches.empty?
1033
+
1034
+ # Load projects to find repo paths
1035
+ projects_file = File.join(BRAINIAC_DIR, "projects.json")
1036
+ return nil unless File.exist?(projects_file)
1037
+
1038
+ projects = JSON.parse(File.read(projects_file))
1039
+
1040
+ epic_branches.each do |project_key, branch_name|
1041
+ repo_path = projects.dig(project_key, "repo_path")
1042
+ next unless repo_path && File.directory?(repo_path)
1043
+
1044
+ # List worktrees and find one with this branch
1045
+ output, status = Open3.capture2("git", "worktree", "list", "--porcelain", chdir: repo_path)
1046
+ next unless status.success?
1047
+
1048
+ # Parse porcelain output
1049
+ current_worktree = nil
1050
+ output.each_line do |line|
1051
+ if line.start_with?("worktree ")
1052
+ current_worktree = line.sub("worktree ", "").strip
1053
+ elsif line.start_with?("branch refs/heads/")
1054
+ wt_branch = line.sub("branch refs/heads/", "").strip
1055
+ return current_worktree if wt_branch == branch_name && current_worktree && current_worktree != repo_path
1056
+ end
1057
+ end
1058
+ end
1059
+
1060
+ nil
1061
+ rescue JSON::ParserError
1062
+ nil
1063
+ end
1064
+
1065
+ def run_deploy(worktree_path, env)
1066
+ deploy_config = Config.deploy
1067
+ command_template = deploy_config["command"] || "belt deploy {env} --auto"
1068
+ command = command_template.gsub("{env}", env)
1069
+
1070
+ puts "Running: #{command}"
1071
+ puts "-" * 40
1072
+
1073
+ # Run the deploy command in the worktree directory
1074
+ system(command, chdir: worktree_path)
1075
+ end
1076
+
876
1077
  def auto_resolve_person_id(agent_name)
877
1078
  output, status = Open3.capture2(
878
1079
  "basecamp", "people", "list", "--jq",
@@ -928,7 +1129,7 @@ module Brainiac
928
1129
  end
929
1130
 
930
1131
  def self.completions
931
- %w[setup config status epics link bot projects set reset webhook]
1132
+ %w[setup config status epics deploy link bot projects set reset webhook]
932
1133
  end
933
1134
  end
934
1135
  end
@@ -20,6 +20,13 @@ module Brainiac
20
20
  "fizzy_account_id" => nil,
21
21
  "review_gate" => "on_complete",
22
22
  "review_gates" => [],
23
+ "deploy" => {
24
+ "enabled" => false,
25
+ "trigger" => "manual",
26
+ "default_env" => nil,
27
+ "project_envs" => {},
28
+ "command" => "belt deploy {env} --auto"
29
+ },
23
30
  "notifications" => {
24
31
  "epic_started" => true,
25
32
  "task_dispatched" => true,
@@ -103,6 +110,13 @@ module Brainiac
103
110
  current["review_gate"] || "on_complete"
104
111
  end
105
112
 
113
+ # Deploy configuration.
114
+ #
115
+ # @return [Hash]
116
+ def deploy
117
+ current["deploy"] || DEFAULT_CONFIG["deploy"]
118
+ end
119
+
106
120
  private
107
121
 
108
122
  def config_mtime
@@ -123,6 +123,37 @@ module Brainiac
123
123
  lines.join("\n")
124
124
  end
125
125
 
126
+ # Extract deploy environment from epic title or description.
127
+ # Supports:
128
+ # [deploy:dev02] — in title (preferred)
129
+ # deploy:dev02 — in description (fallback)
130
+ #
131
+ # @param title [String] Epic/todolist title
132
+ # @param description [String, nil] Optional description to check as fallback
133
+ # @return [String, nil] Environment name or nil
134
+ def extract_deploy_env(title, description = nil)
135
+ # Try [deploy:env] format in title (preferred)
136
+ if title
137
+ marker = "[deploy:"
138
+ value_start = title.index(marker)
139
+ if value_start
140
+ value_start += marker.length
141
+ value_end = title.index("]", value_start)
142
+ if value_end
143
+ environment = title[value_start...value_end].strip
144
+ return environment unless environment.empty?
145
+ end
146
+ end
147
+ end
148
+
149
+ # Fallback: try deploy:env in description
150
+ if description && (match = description.match(/deploy:(\S+)/i))
151
+ return match[1].strip
152
+ end
153
+
154
+ nil
155
+ end
156
+
126
157
  private
127
158
 
128
159
  # Extract Fizzy card number from title.
@@ -737,6 +737,9 @@ module Brainiac
737
737
  # If epic_branch mode, open final PRs to main
738
738
  if epic["review_gate"] == "epic_branch" && epic["epic_branches"]&.any?
739
739
  open_final_prs(epic)
740
+
741
+ # Trigger automated deploy if configured
742
+ trigger_deploy_if_configured(epic, "on_final_pr")
740
743
  end
741
744
 
742
745
  # Post a summary message in Basecamp
@@ -925,6 +928,130 @@ module Brainiac
925
928
  message: message,
926
929
  agent: agent)
927
930
  end
931
+
932
+ # Trigger automated deploy if configured for the given trigger point.
933
+ #
934
+ # @param epic [Hash] Epic state
935
+ # @param trigger [String] Trigger point ("on_final_pr", "on_task_merge", etc.)
936
+ def trigger_deploy_if_configured(epic, trigger)
937
+ deploy_config = Config.deploy
938
+ return unless deploy_config["enabled"]
939
+ return unless deploy_config["trigger"] == trigger
940
+
941
+ # Resolve deploy environment
942
+ deploy_env = resolve_epic_deploy_env(epic)
943
+ unless deploy_env
944
+ LOG.warn "[Basecamp:Orchestrator] Deploy triggered but no environment configured for epic '#{epic['title']}'" if defined?(LOG)
945
+ return
946
+ end
947
+
948
+ # Find worktree
949
+ worktree_path = find_epic_worktree(epic)
950
+ unless worktree_path
951
+ LOG.warn "[Basecamp:Orchestrator] Deploy triggered but no worktree found for epic '#{epic['title']}'" if defined?(LOG)
952
+ return
953
+ end
954
+
955
+ LOG.info "[Basecamp:Orchestrator] Auto-deploying epic '#{epic['title']}' to #{deploy_env}" if defined?(LOG)
956
+
957
+ # Run deploy
958
+ command_template = deploy_config["command"] || "belt deploy {env} --auto"
959
+ command = command_template.gsub("{env}", deploy_env)
960
+
961
+ success = system(command, chdir: worktree_path)
962
+
963
+ # Record deploy result
964
+ epic["last_deploy"] = {
965
+ "env" => deploy_env,
966
+ "at" => Time.now.iso8601,
967
+ "status" => success ? "success" : "failed",
968
+ "trigger" => trigger
969
+ }
970
+
971
+ # Send notification
972
+ if success
973
+ send_notification(
974
+ event: :epic_deployed,
975
+ message: "🚀 Epic **#{epic['title']}** deployed to **#{deploy_env}**",
976
+ agent: epic["agent"]
977
+ )
978
+ else
979
+ send_notification(
980
+ event: :epic_deploy_failed,
981
+ message: "❌ Epic **#{epic['title']}** deploy to **#{deploy_env}** failed",
982
+ agent: epic["agent"]
983
+ )
984
+ end
985
+ rescue StandardError => e
986
+ LOG.error "[Basecamp:Orchestrator] Deploy failed: #{e.message}" if defined?(LOG)
987
+ epic["last_deploy"] = {
988
+ "env" => deploy_env,
989
+ "at" => Time.now.iso8601,
990
+ "status" => "error",
991
+ "error" => e.message,
992
+ "trigger" => trigger
993
+ }
994
+ end
995
+
996
+ # Resolve deploy environment for an epic.
997
+ #
998
+ # @param epic [Hash] Epic state
999
+ # @return [String, nil] Environment name or nil
1000
+ def resolve_epic_deploy_env(epic)
1001
+ # 1. [deploy:env] in title or deploy:env in description
1002
+ env = Epic.extract_deploy_env(epic["title"], epic["description"])
1003
+ return env if env
1004
+
1005
+ # 2. Per-project default from config
1006
+ # Prefer the project from tasks (set from Fizzy card tags) over basecamp mapping
1007
+ # since multiple brainiac projects can share the same basecamp project.
1008
+ project_key = (epic["tasks"] || []).map { |t| t["project"] }.compact.first
1009
+ project_key ||= Config.brainiac_project_for(epic["basecamp_project_id"])
1010
+ project_env = Config.deploy.dig("project_envs", project_key) if project_key
1011
+ return project_env if project_env
1012
+
1013
+ # 3. Global default
1014
+ Config.deploy["default_env"]
1015
+ end
1016
+
1017
+ # Find the worktree path for an epic's branch.
1018
+ #
1019
+ # @param epic [Hash] Epic state
1020
+ # @return [String, nil] Worktree path or nil
1021
+ def find_epic_worktree(epic)
1022
+ epic_branches = epic["epic_branches"] || {}
1023
+ return nil if epic_branches.empty?
1024
+
1025
+ projects_file = File.join(BRAINIAC_DIR, "projects.json")
1026
+ return nil unless File.exist?(projects_file)
1027
+
1028
+ projects = JSON.parse(File.read(projects_file))
1029
+
1030
+ epic_branches.each do |project_key, branch_name|
1031
+ repo_path = projects.dig(project_key, "repo_path")
1032
+ next unless repo_path && File.directory?(repo_path)
1033
+
1034
+ # List worktrees and find one with this branch
1035
+ output, status = Open3.capture2("git", "worktree", "list", "--porcelain", chdir: repo_path)
1036
+ next unless status.success?
1037
+
1038
+ current_worktree = nil
1039
+ output.each_line do |line|
1040
+ if line.start_with?("worktree ")
1041
+ current_worktree = line.sub("worktree ", "").strip
1042
+ elsif line.start_with?("branch refs/heads/")
1043
+ wt_branch = line.sub("branch refs/heads/", "").strip
1044
+ if wt_branch == branch_name && current_worktree && current_worktree != repo_path
1045
+ return current_worktree
1046
+ end
1047
+ end
1048
+ end
1049
+ end
1050
+
1051
+ nil
1052
+ rescue JSON::ParserError
1053
+ nil
1054
+ end
928
1055
  end
929
1056
  end
930
1057
  end
@@ -59,8 +59,11 @@ module Brainiac
59
59
 
60
60
  LOG.info "[Basecamp:SessionRegistry] Registered session: #{task_id} (pid=#{pid}, agent=#{agent_name})" if defined?(LOG)
61
61
 
62
- # Also forward to the global register_session if it exists (for waybar/UI)
63
- if Object.respond_to?(:register_session, true) && !@suppress_global_forward
62
+ # Also forward to the global register_session if it exists (for waybar/UI).
63
+ # Skip implementation-* sessions — fizzy already registers those as card-NNNN
64
+ # and we don't want duplicate entries in the tray.
65
+ if Object.respond_to?(:register_session, true) && !@suppress_global_forward &&
66
+ !task_id.to_s.start_with?("implementation-")
64
67
  begin
65
68
  Object.send(:register_session, task_id, pid, log_file: log_file, agent_name: agent_name)
66
69
  rescue StandardError
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Basecamp
6
- VERSION = "0.0.19"
6
+ VERSION = "0.0.21"
7
7
  end
8
8
  end
9
9
  end
@@ -115,9 +115,16 @@ module Brainiac
115
115
  reconcile_task(epic, task, triggered_by: triggered_by)
116
116
  end.any?
117
117
 
118
- Orchestrator.send(:dispatch_unblocked_tasks, epic) if changed
119
- Orchestrator.send(:save_epic, epic) if changed
120
- changed
118
+ # Always attempt to dispatch unblocked tasks during recovery. Even if no
119
+ # individual task changed state, there may be pending tasks whose
120
+ # dependencies are satisfied that were never dispatched (e.g., after a
121
+ # cancellation was reversed or an epic was healed manually).
122
+ has_pending = tasks.any? { |t| TaskState.in?(t, :pending) }
123
+ if changed || has_pending
124
+ Orchestrator.send(:dispatch_unblocked_tasks, epic)
125
+ Orchestrator.send(:save_epic, epic)
126
+ end
127
+ changed || has_pending
121
128
  end
122
129
 
123
130
  private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac-basecamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.19
4
+ version: 0.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis