brainiac-fizzy 0.0.13 → 0.0.15

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: 0e09da26fb3e4d7a37ef7fff687ad5b5e094d6cad508227fac32467273541619
4
- data.tar.gz: 4de9000657e8afd645e5dcd04b02c9522912fb2df1428ffca10e4235b9d1b462
3
+ metadata.gz: 554fe8796537111b5df0a3d5236cb457c28089097f75efb797fdb2a32fa20c6a
4
+ data.tar.gz: 5a1799ef50971dc53df499213fa00bae7d84c1913da5da070b4c4e96de8e113c
5
5
  SHA512:
6
- metadata.gz: 3fe71aad4fec21bb3430d71bc9461156779b590c465bb49ec7199e6a4183f9321af16d98b3b6ffd9a01dc8505dd8d8708c9f0d1dfbdef98dd7e3b8c7fe3cea96
7
- data.tar.gz: 22cff590e92a0455820feb9f40a0eacf9552f9989ccde9b155e193673c0260f108771469c4d45b739c72626a87927bd79108d2063b85c2fe0aab981681b2f0bd
6
+ metadata.gz: f2cc672d0cd0760c0c4319a9b5bca323877b8ca7284739a3cd8fbd4f6deeffb0735e7bd9c19d50726b62207453bffab2b228f972b55c1f0ac1289a0490155a12
7
+ data.tar.gz: 3e7e6d3da09df5f97bfbe47b7b152dca2381c58b3ed75d315ff8979f0341b62563dbae33043d568562c23de06e9cee10d07ce54e2b290bd8d13fbf9c0d322437
@@ -33,8 +33,8 @@ def move_card_to_column(card_number, column_name, project_config:, agent_name: n
33
33
  Brainiac::Plugins::Fizzy::Helpers.move_card_to_column(card_number, column_name, project_config: project_config, agent_name: agent_name)
34
34
  end
35
35
 
36
- def append_fizzy_comment_footer(card_number, project_config:, agent_name: nil)
37
- Brainiac::Plugins::Fizzy::Helpers.append_fizzy_comment_footer(card_number, project_config: project_config, agent_name: agent_name)
36
+ def append_fizzy_comment_footer(card_number, project_config:, agent_name: nil, since: nil)
37
+ Brainiac::Plugins::Fizzy::Helpers.append_fizzy_comment_footer(card_number, project_config: project_config, agent_name: agent_name, since: since)
38
38
  end
39
39
 
40
40
  def ensure_fizzy_yaml!(chdir, project_config)
@@ -136,6 +136,14 @@ PROMPT_MENTION = Brainiac::Plugins::Fizzy::Prompts::MENTION
136
136
  PROMPT_CROSS_AGENT_REVIEW = Brainiac::Plugins::Fizzy::Prompts::CROSS_AGENT_REVIEW
137
137
  PROMPT_PLANNING_MODE = Brainiac::Plugins::Fizzy::Prompts::PLANNING_MODE
138
138
 
139
+ # Default Fizzy board column IDs — used as fallback when board_key is nil or
140
+ # the board config doesn't specify column mappings.
141
+ DEFAULT_COLUMN_IDS = {
142
+ "right_now" => "03f5xa5q9fog9592pa1279dts",
143
+ "needs_review" => "03f5ykobhpsd78hbuvajtn8g8",
144
+ "uat" => "03fsmglsr6az06ppyotawsti8"
145
+ }.freeze
146
+
139
147
  # Render planning mode prompt — identical to render_prompt but inserts the planning
140
148
  # instructions between PROMPT_CORE and the channel rules. This was originally defined
141
149
  # in brainiac core's planning.rb but belongs here after the fizzy extraction.
@@ -152,7 +152,7 @@ def dispatch_assigned_card(card_number:, card_internal_id:, title:, tags:, branc
152
152
  project_config: project_config, chdir: worktree_path,
153
153
  log_name: "assigned-#{card_number}", model: model, effort: effort,
154
154
  agent_name: agent_name, card_number: card_number, source: :fizzy,
155
- source_context: { card_number: card_number },
155
+ source_context: { card_number: card_number, dispatched_at: Time.now },
156
156
  cli_provider: cli_provider_override)
157
157
  register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: agent_name)
158
158
 
@@ -408,7 +408,7 @@ def dispatch_cross_agent_review(ctx, card_key:, card_number:, card_assigned_agen
408
408
  log_name: "review-#{ctx.agent_name.downcase}-#{card_number || ctx.card_internal_id}",
409
409
  model: ctx.model, effort: ctx.effort, agent_name: ctx.agent_name,
410
410
  card_number: card_number, comment_id: ctx.comment_id,
411
- source: :fizzy, source_context: { card_number: card_number },
411
+ source: :fizzy, source_context: { card_number: card_number, dispatched_at: Time.now },
412
412
  cli_provider: ctx.cli_provider_override)
413
413
  return [200, { status: "dispatch_failed", agent: ctx.agent_name, card: card_number }.to_json] unless pid
414
414
 
@@ -514,7 +514,7 @@ def dispatch_new_mention(ctx, card_key:, card_number:, card_title:, branch:, wor
514
514
  model: ctx.model, effort: ctx.effort, agent_name: ctx.agent_name,
515
515
  card_number: card_number, comment_id: ctx.comment_id,
516
516
  source: :fizzy, cli_provider: ctx.cli_provider_override,
517
- source_context: { card_number: card_number })
517
+ source_context: { card_number: card_number, dispatched_at: Time.now })
518
518
  return [200, { status: "dispatch_failed", agent: ctx.agent_name, card: card_number }.to_json] unless pid
519
519
 
520
520
  register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: ctx.agent_name)
@@ -554,7 +554,7 @@ def dispatch_followup_comment(ctx, card_key:, card_number:, work_dir:)
554
554
  source: :fizzy, cli_provider: ctx.cli_provider_override, resume: should_resume,
555
555
  source_context: {
556
556
  card_number: card_number, card_internal_id: ctx.card_internal_id,
557
- deploy_intent: ctx.deploy_intent
557
+ deploy_intent: ctx.deploy_intent, dispatched_at: Time.now
558
558
  })
559
559
  return [200, { status: "dispatch_failed", agent: ctx.agent_name, card: card_number }.to_json] unless pid
560
560
 
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Brainiac
6
+ module Plugins
7
+ module Fizzy
8
+ # Handles re-dispatch summarize sessions and fallback comment posting.
9
+ # Extracted from Hooks to keep module length manageable.
10
+ module Summarize
11
+ class << self
12
+ # Re-dispatch an agent to post a summary comment when it completed work
13
+ # but didn't comment on the card. Fires async with a 60s timeout.
14
+ # If this session also fails to comment, posts a minimal fallback.
15
+ def dispatch_summarize_session(ctx)
16
+ card_number = ctx[:card_number]
17
+ agent_name = ctx[:agent_name]
18
+ project_config = ctx[:project_config]
19
+ repo_path = project_config["repo_path"]
20
+ session_started_at = ctx[:source_context]&.dig(:dispatched_at)
21
+
22
+ map = load_work_item_map
23
+ work_item = find_work_item(map, card_number)
24
+
25
+ branch = work_item&.dig("branch") || "unknown"
26
+ card_title = work_item&.dig("title") || ctx[:source_context]&.dig(:card_title) || "untitled"
27
+ worktree_path = work_item&.dig("worktree") || repo_path
28
+
29
+ if defined?(LOG)
30
+ LOG.info "[Fizzy] Agent #{agent_name} completed card ##{card_number} without commenting — " \
31
+ "dispatching summarize session"
32
+ end
33
+
34
+ pid, log_file = dispatch_summarize_agent(card_number, card_title, branch, agent_name,
35
+ project_config, worktree_path)
36
+ register_session("card-#{card_number}-summarize", pid, log_file: log_file, agent_name: agent_name)
37
+
38
+ monitor_summarize_session(pid, card_number, agent_name, branch, repo_path, session_started_at,
39
+ project_config)
40
+ rescue StandardError => e
41
+ LOG.error "[Fizzy] Failed to dispatch summarize session for card ##{card_number}: #{e.message}" if defined?(LOG)
42
+ post_fallback_comment(card_number, work_item&.dig("branch") || "unknown", agent_name, project_config)
43
+ end
44
+
45
+ # Check if the agent posted a comment on the card since a given time.
46
+ # If no `since` is provided, checks for any agent comment (legacy behavior).
47
+ # Subtracts a 30s buffer from `since` to account for clock skew between
48
+ # the local server and the Fizzy API.
49
+ CLOCK_SKEW_BUFFER = 30
50
+
51
+ def agent_commented_on_card?(card_number, agent_name, repo_path:, since: nil)
52
+ env = Helpers.fizzy_env_for(agent_name)
53
+ output = run_cmd("fizzy", "comment", "list", "--card", card_number.to_s, chdir: repo_path, env: env)
54
+ comments = JSON.parse(output)["data"] || []
55
+ agent_display = agent_display_name(agent_name)
56
+
57
+ agent_comments = comments.select { |c| c["creator_name"]&.downcase == agent_display.downcase }
58
+ return agent_comments.any? unless since
59
+
60
+ buffered_since = since - CLOCK_SKEW_BUFFER
61
+ agent_comments.any? do |c|
62
+ comment_time = c["created_at"] && Time.parse(c["created_at"])
63
+ comment_time && comment_time > buffered_since
64
+ end
65
+ rescue StandardError
66
+ false
67
+ end
68
+
69
+ # Post a minimal server-generated comment when all else fails.
70
+ def post_fallback_comment(card_number, branch, agent_name, project_config)
71
+ repo_path = project_config["repo_path"]
72
+ env = Helpers.fizzy_env_for(agent_name)
73
+
74
+ pr_url = Helpers.send(:detect_pr_url, branch, project_config)
75
+ body = "<p>✅ Work completed on this card.</p>"
76
+ body += "<p><a href=\"#{pr_url}\">View PR</a></p>" if pr_url
77
+ body += "<p><strong>Branch:</strong> <code>#{branch}</code></p>"
78
+
79
+ run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", body,
80
+ chdir: repo_path, env: env)
81
+ LOG.info "[Fizzy] Posted fallback comment on card ##{card_number}" if defined?(LOG)
82
+ rescue StandardError => e
83
+ LOG.error "[Fizzy] Failed to post fallback comment on card ##{card_number}: #{e.message}" if defined?(LOG)
84
+ end
85
+
86
+ private
87
+
88
+ def find_work_item(map, card_number)
89
+ map.values.find do |info|
90
+ info.is_a?(Hash) &&
91
+ (info.dig("sources", "fizzy", "card_number").to_s == card_number.to_s ||
92
+ info["number"].to_s == card_number.to_s)
93
+ end
94
+ end
95
+
96
+ def dispatch_summarize_agent(card_number, card_title, branch, agent_name, project_config, worktree_path)
97
+ prompt = render_prompt(Prompts::SUMMARIZE_WORK,
98
+ { "CARD_NUMBER" => card_number, "CARD_TITLE" => card_title, "BRANCH" => branch },
99
+ brain_context: "", agent_name: agent_name, channel: :fizzy,
100
+ board_key: Config.board_key_for_project(project_config))
101
+
102
+ run_agent(prompt,
103
+ project_config: project_config, chdir: worktree_path,
104
+ log_name: "summarize-#{card_number}", agent_name: agent_name,
105
+ source: :fizzy,
106
+ source_context: { card_number: card_number, skip_summarize_redispatch: true },
107
+ skip_column_move: true)
108
+ end
109
+
110
+ def monitor_summarize_session(pid, card_number, agent_name, branch, repo_path, session_started_at,
111
+ project_config)
112
+ Thread.new do
113
+ wait_for_process_or_timeout(pid, timeout: 60)
114
+ sleep 2 # Brief pause to let any comment propagate
115
+
116
+ unless agent_commented_on_card?(card_number, agent_name, repo_path: repo_path, since: session_started_at)
117
+ post_fallback_comment(card_number, branch, agent_name, project_config)
118
+ end
119
+ rescue Errno::ECHILD
120
+ # Process already reaped by session manager — check for comment
121
+ sleep 2
122
+ unless agent_commented_on_card?(card_number, agent_name, repo_path: repo_path, since: session_started_at)
123
+ post_fallback_comment(card_number, branch, agent_name, project_config)
124
+ end
125
+ rescue StandardError => e
126
+ LOG.error "[Fizzy] Summarize monitor error for card ##{card_number}: #{e.message}" if defined?(LOG)
127
+ end
128
+ end
129
+
130
+ def wait_for_process_or_timeout(pid, timeout: 60)
131
+ deadline = Time.now + timeout
132
+ loop do
133
+ result = Process.waitpid2(pid, Process::WNOHANG)
134
+ break if result
135
+
136
+ if Time.now > deadline
137
+ safe_kill(pid)
138
+ break
139
+ end
140
+ sleep 1
141
+ end
142
+ end
143
+
144
+ def safe_kill(pid)
145
+ Process.kill("TERM", pid)
146
+ Process.waitpid2(pid)
147
+ rescue Errno::ESRCH, Errno::ECHILD
148
+ # Process already gone
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -102,7 +102,7 @@ module Brainiac
102
102
  run_cmd("fizzy", "card", "column", card_number.to_s, "--column", column_id, chdir: repo_path, env: env)
103
103
  end
104
104
 
105
- def append_fizzy_comment_footer(card_number, project_config:, agent_name: nil)
105
+ def append_fizzy_comment_footer(card_number, project_config:, agent_name: nil, since: nil)
106
106
  repo_path = project_config["repo_path"]
107
107
  env = fizzy_env_for(agent_name || AI_AGENT_NAME)
108
108
 
@@ -110,29 +110,40 @@ module Brainiac
110
110
  comments = JSON.parse(output)["data"] || []
111
111
  agent_display = agent_display_name(agent_name || AI_AGENT_NAME)
112
112
 
113
- last_agent_comment = comments.reverse.find do |c|
114
- c["creator_name"]&.downcase == agent_display.downcase
113
+ # Find the most recent agent comment, scoped to this session if `since` is provided.
114
+ # Subtracts 30s buffer from `since` to account for clock skew between local server and Fizzy API.
115
+ agent_comments = comments.select { |c| c["creator_name"]&.downcase == agent_display.downcase }
116
+ if since
117
+ buffered_since = since - 30
118
+ agent_comments = agent_comments.select do |c|
119
+ comment_time = c["created_at"] && Time.parse(c["created_at"])
120
+ comment_time && comment_time > buffered_since
121
+ end
115
122
  end
116
- return unless last_agent_comment
117
-
118
- # Check if footer already exists
119
- body = last_agent_comment.dig("body", "html") || ""
120
- return if body.include?("<em>Branch:")
121
123
 
122
- # Detect branch from comment content or card map
123
- branch = detect_branch_from_comment(body, card_number)
124
- return unless branch
124
+ last_agent_comment = agent_comments.last
125
+ return false unless last_agent_comment
125
126
 
126
- pr_url = detect_pr_url(branch, project_config)
127
- footer = "<p><em>Branch: <code>#{branch}</code>"
128
- footer += " | <a href=\"#{pr_url}\">PR</a>" if pr_url
129
- footer += "</em></p>"
127
+ # Append footer if applicable
128
+ body = last_agent_comment.dig("body", "html") || ""
129
+ unless body.include?("<em>Branch:")
130
+ branch = detect_branch_from_comment(body, card_number)
131
+ if branch
132
+ pr_url = detect_pr_url(branch, project_config)
133
+ footer = "<p><em>Branch: <code>#{branch}</code>"
134
+ footer += " | <a href=\"#{pr_url}\">PR</a>" if pr_url
135
+ footer += "</em></p>"
136
+
137
+ updated_body = body + footer
138
+ run_cmd("fizzy", "comment", "update", last_agent_comment["id"], "--card", card_number.to_s,
139
+ "--body", updated_body, chdir: repo_path, env: env)
140
+ end
141
+ end
130
142
 
131
- updated_body = body + footer
132
- run_cmd("fizzy", "comment", "update", last_agent_comment["id"], "--card", card_number.to_s,
133
- "--body", updated_body, chdir: repo_path, env: env)
143
+ true
134
144
  rescue StandardError => e
135
145
  LOG.warn "[Fizzy] Could not append footer to card ##{card_number}: #{e.message}" if defined?(LOG)
146
+ false
136
147
  end
137
148
 
138
149
  def ensure_fizzy_yaml!(chdir, project_config)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+
3
5
  module Brainiac
4
6
  module Plugins
5
7
  module Fizzy
@@ -25,6 +27,7 @@ module Brainiac
25
27
  private
26
28
 
27
29
  # After an agent session completes — move card to needs_review, append footer
30
+ # If the agent didn't post a comment, re-dispatch for a summary.
28
31
  def register_agent_completed
29
32
  Brainiac.on(:agent_completed) do |ctx|
30
33
  next unless ctx[:source] == :fizzy
@@ -39,9 +42,18 @@ module Brainiac
39
42
  record_self_move(card_number)
40
43
  end
41
44
 
42
- Helpers.append_fizzy_comment_footer(card_number,
43
- project_config: ctx[:project_config],
44
- agent_name: ctx[:agent_name])
45
+ found_comment = Helpers.append_fizzy_comment_footer(card_number,
46
+ project_config: ctx[:project_config],
47
+ agent_name: ctx[:agent_name],
48
+ since: ctx[:source_context]&.dig(:dispatched_at))
49
+
50
+ # If the agent didn't post any comment during this session, re-dispatch to summarize from memory.
51
+ # Uses dispatched_at from source_context to only check for comments made during THIS session,
52
+ # not previous sessions on the same card.
53
+ if !found_comment &&
54
+ !ctx[:source_context]&.dig(:skip_summarize_redispatch)
55
+ dispatch_summarize_session(ctx)
56
+ end
45
57
 
46
58
  # Planning mode finalization
47
59
  Planning.finalize_if_needed(ctx[:prompt_file], ctx[:agent_name], ctx[:project_config])
@@ -240,13 +252,28 @@ module Brainiac
240
252
 
241
253
  pid, log_file = run_agent(prompt, project_config: project_config, chdir: repo_path,
242
254
  log_name: "uat-#{card_number}", agent_name: agent_name,
243
- source: :fizzy, source_context: { card_number: card_number }, skip_column_move: true)
255
+ source: :fizzy,
256
+ source_context: { card_number: card_number, dispatched_at: Time.now },
257
+ skip_column_move: true)
244
258
  register_session("card-#{card_number}", pid, log_file: log_file, agent_name: agent_name)
245
259
  LOG.info "[Fizzy] Dispatched #{agent_name} for UAT on card ##{card_number}" if defined?(LOG)
246
260
  rescue StandardError => e
247
261
  LOG.error "[Fizzy] Failed to dispatch UAT agent: #{e.message}" if defined?(LOG)
248
262
  end
249
263
 
264
+ # Delegated to Summarize module for manageable file length.
265
+ def dispatch_summarize_session(ctx)
266
+ Summarize.dispatch_summarize_session(ctx)
267
+ end
268
+
269
+ def agent_commented_on_card?(card_number, agent_name, repo_path:, since: nil)
270
+ Summarize.agent_commented_on_card?(card_number, agent_name, repo_path: repo_path, since: since)
271
+ end
272
+
273
+ def post_fallback_comment(card_number, branch, agent_name, project_config)
274
+ Summarize.post_fallback_comment(card_number, branch, agent_name, project_config)
275
+ end
276
+
250
277
  def close_uat_cards(card_list, repo_path)
251
278
  closed = []
252
279
  map = load_work_item_map
@@ -55,7 +55,9 @@ module Brainiac
55
55
  You have been assigned Fizzy card #{{CARD_NUMBER}}: "{{CARD_TITLE}}".
56
56
  You are on branch "{{BRANCH}}" in a fresh worktree.
57
57
  Implement the task, commit, push, and open a PR (link back to Fizzy).
58
- When you're done, post a comment on the card with a concise summary, PR link, and branch name.
58
+
59
+ **Response destination: Post your response as a comment on Fizzy card #{{CARD_NUMBER}}.**
60
+ Your comment MUST include a concise summary of what you did, a PR link, and the branch name.
59
61
 
60
62
  **MANDATORY: Always include the branch name in your comment.** Use this format:
61
63
  `<p><strong>Branch:</strong> <code>{{BRANCH}}</code></p>`
@@ -111,6 +113,18 @@ module Brainiac
111
113
  **IMPORTANT: Do NOT @mention any other agents in your response.**
112
114
  PROMPT
113
115
 
116
+ SUMMARIZE_WORK = <<~'PROMPT'
117
+ You completed work on Fizzy card #{{CARD_NUMBER}} ("{{CARD_TITLE}}") but did not post a comment.
118
+ Read your memory file for this card and write a brief summary comment.
119
+
120
+ **Response destination: Post your response as a comment on Fizzy card #{{CARD_NUMBER}}.**
121
+ Include what you did, any PR link, and the branch name.
122
+ Keep it concise — this is a summary, not a full report.
123
+
124
+ **MANDATORY: Always include the branch name in your comment.** Use this format:
125
+ `<p><strong>Branch:</strong> <code>{{BRANCH}}</code></p>`
126
+ PROMPT
127
+
114
128
  UAT_TESTING = <<~'PROMPT'
115
129
  PR for card #{{CARD_NUMBER}} ("{{CARD_TITLE}}") has been merged to main (PR #{{PR_NUMBER}}).
116
130
  Run any UAT testing steps defined in the card or acceptance criteria.
@@ -3,7 +3,7 @@
3
3
  module Brainiac
4
4
  module Plugins
5
5
  module Fizzy
6
- VERSION = "0.0.13"
6
+ VERSION = "0.0.15"
7
7
  end
8
8
  end
9
9
  end
@@ -16,6 +16,7 @@ require_relative "fizzy/handlers/dedup"
16
16
  require_relative "fizzy/handlers/deploy"
17
17
  require_relative "fizzy/handlers/card_index"
18
18
  require_relative "fizzy/handlers/deployments"
19
+ require_relative "fizzy/handlers/summarize"
19
20
 
20
21
  module Brainiac
21
22
  module Plugins
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.13
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis
@@ -98,6 +98,7 @@ files:
98
98
  - lib/brainiac/plugins/fizzy/handlers/dedup.rb
99
99
  - lib/brainiac/plugins/fizzy/handlers/deploy.rb
100
100
  - lib/brainiac/plugins/fizzy/handlers/deployments.rb
101
+ - lib/brainiac/plugins/fizzy/handlers/summarize.rb
101
102
  - lib/brainiac/plugins/fizzy/helpers.rb
102
103
  - lib/brainiac/plugins/fizzy/hooks.rb
103
104
  - lib/brainiac/plugins/fizzy/metadata.rb