brainiac-fizzy 0.0.12 → 0.0.14

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: c0335c88018a40d0f597d0c7000d56e4fe047540a077e2d17b85b20819d730b1
4
- data.tar.gz: c4b4262ef5b7d95687302edd2fb9864e93b3c4dd12f8eb0a8ea6a6dabd73536c
3
+ metadata.gz: 90ffad33201985cb5a94b3460fb635d254e806e435543eeb652b533ff59562a6
4
+ data.tar.gz: 209e7829523442c8ccc7e3a278454ddd008fd16df6f83495e1edd42c0ac9dada
5
5
  SHA512:
6
- metadata.gz: ce588f2a8a5284d681d9dce3340aa90e8372039e60bbfe53a8cd4dbb1d1de6e28e37a86af7f075d5ed2e7618b52e7eec69de3ee100336f7679103a15acc6a0cd
7
- data.tar.gz: 1d92e2d6d185dbd492826d1efe6d7a9c62ff2b5bda3e07a44b2ce4a803bc997ace21c77d03fe9f7c4d131b823e020d3dd2cd88e0f08e8e680d0c452a132bb494
6
+ metadata.gz: 207769fa7579c519e38ff9551a5b4fb0ff823c1cb88740b7bd3b7f1a459bfd8c7e9ea97e62fd7a6dbeb8a48218595d08143564be8cd577ff32a264be7a97d947
7
+ data.tar.gz: e5d52c3fc6dcf2427b70030fccdb3a9196b2c00ecf63b934710accbaa3cdfef16a7c98765afac1ae33435d86f58837371097b697e733d8484f8bdee6efdedef7
@@ -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.
@@ -153,11 +161,9 @@ def render_planning_prompt(situation_template, vars = {}, brain_context: "", car
153
161
  result += plugin_prompt || CHANNEL_PROMPTS.fetch(channel, "")
154
162
  result += situation_template
155
163
 
156
- # Pre-post comment check (same as render_prompt)
157
- case channel
158
- when :fizzy then result += PROMPT_PRE_POST_CHECK_FIZZY
159
- when :github then result += PROMPT_PRE_POST_CHECK_GITHUB
160
- end
164
+ # Pre-post comment check (same as render_prompt — use plugin-registered lookup)
165
+ plugin_pre_post = Brainiac.channel_pre_post_checks[channel]
166
+ result += plugin_pre_post if plugin_pre_post
161
167
 
162
168
  result += PROMPT_REFLECTION
163
169
 
@@ -174,12 +180,7 @@ def render_planning_prompt(situation_template, vars = {}, brain_context: "", car
174
180
  planning_vars[var_name] ||= (board_key && board_column_id(board_key, col_name)) || default_id
175
181
  end
176
182
 
177
- # Touch memory file if CARD_ID is present — ensures file exists before agent tries to read it
178
- if vars["CARD_ID"]
179
- memory_file = File.join(planning_vars["MEMORY_DIR"], "card-#{vars["CARD_ID"]}.md")
180
- FileUtils.mkdir_p(planning_vars["MEMORY_DIR"])
181
- FileUtils.touch(memory_file)
182
- end
183
+ ensure_memory_file_exists!(vars["CARD_ID"], planning_vars["MEMORY_DIR"])
183
184
 
184
185
  roster = agent_roster
185
186
  roster_lines = roster.map { |_key, display| " - @#{display}" }.join("\n")
@@ -189,6 +190,15 @@ def render_planning_prompt(situation_template, vars = {}, brain_context: "", car
189
190
  result
190
191
  end
191
192
 
193
+ # Ensure a card's memory file exists so the agent can read it without error.
194
+ def ensure_memory_file_exists!(card_id, memory_dir)
195
+ return unless card_id
196
+
197
+ memory_file = File.join(memory_dir, "card-#{card_id}.md")
198
+ FileUtils.mkdir_p(memory_dir)
199
+ FileUtils.touch(memory_file)
200
+ end
201
+
192
202
  # Config constants — handler files reference these as top-level constants.
193
203
  # These are evaluated after Config.load! has been called (during plugin register).
194
204
  # Use a delegating object so it always reflects current config state.
@@ -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
 
@@ -78,9 +78,11 @@ def handle_comment(payload)
78
78
  directly_addressed = mentioned_agent || (card_info && card_info["agent"]&.downcase == agent_name.downcase)
79
79
  unless directly_addressed
80
80
  card_number_for_intent = card_info&.dig("number") || eventable.dig("card", "number")
81
- intent_ctx = card_number_for_intent ? fetch_intent_context(card_number_for_intent, repo_path: project_config["repo_path"], agent_name: agent_name) : nil
81
+ intent_ctx = if card_number_for_intent
82
+ fetch_intent_context(card_number_for_intent, repo_path: project_config["repo_path"], agent_name: agent_name)
83
+ end
82
84
  if intent_skip?(ctx.plain_text, agent_name: agent_name, source: :fizzy,
83
- channel: "Fizzy card ##{card_number_for_intent || card_internal_id}", context: intent_ctx)
85
+ channel: "Fizzy card ##{card_number_for_intent || card_internal_id}", context: intent_ctx)
84
86
  return [200, { status: "intent_skip", agent: agent_name }.to_json]
85
87
  end
86
88
  end
@@ -406,7 +408,7 @@ def dispatch_cross_agent_review(ctx, card_key:, card_number:, card_assigned_agen
406
408
  log_name: "review-#{ctx.agent_name.downcase}-#{card_number || ctx.card_internal_id}",
407
409
  model: ctx.model, effort: ctx.effort, agent_name: ctx.agent_name,
408
410
  card_number: card_number, comment_id: ctx.comment_id,
409
- source: :fizzy, source_context: { card_number: card_number },
411
+ source: :fizzy, source_context: { card_number: card_number, dispatched_at: Time.now },
410
412
  cli_provider: ctx.cli_provider_override)
411
413
  return [200, { status: "dispatch_failed", agent: ctx.agent_name, card: card_number }.to_json] unless pid
412
414
 
@@ -512,7 +514,7 @@ def dispatch_new_mention(ctx, card_key:, card_number:, card_title:, branch:, wor
512
514
  model: ctx.model, effort: ctx.effort, agent_name: ctx.agent_name,
513
515
  card_number: card_number, comment_id: ctx.comment_id,
514
516
  source: :fizzy, cli_provider: ctx.cli_provider_override,
515
- source_context: { card_number: card_number })
517
+ source_context: { card_number: card_number, dispatched_at: Time.now })
516
518
  return [200, { status: "dispatch_failed", agent: ctx.agent_name, card: card_number }.to_json] unless pid
517
519
 
518
520
  register_session(card_key, pid, log_file: log_file, supersede_key: card_key, agent_name: ctx.agent_name)
@@ -532,9 +534,7 @@ def dispatch_followup_comment(ctx, card_key:, card_number:, work_dir:)
532
534
  agent_name: ctx.agent_name, chdir: work_dir
533
535
  )
534
536
 
535
- if ctx.fresh && is_worktree
536
- LOG.info "[Fizzy] [fresh] tag — forcing new session instead of resuming on card #{card_number || ctx.card_internal_id}"
537
- end
537
+ LOG.info "[Fizzy] [fresh] — forcing new session instead of resuming on card #{card_number || ctx.card_internal_id}" if ctx.fresh && is_worktree
538
538
 
539
539
  prompt = if should_resume
540
540
  LOG.info "[Resume] Using lean prompt for follow-up on card #{card_number || ctx.card_internal_id}"
@@ -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,149 @@
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
+ def agent_commented_on_card?(card_number, agent_name, repo_path:, since: nil)
48
+ env = Helpers.fizzy_env_for(agent_name)
49
+ output = run_cmd("fizzy", "comment", "list", "--card", card_number.to_s, chdir: repo_path, env: env)
50
+ comments = JSON.parse(output)["data"] || []
51
+ agent_display = agent_display_name(agent_name)
52
+
53
+ agent_comments = comments.select { |c| c["creator_name"]&.downcase == agent_display.downcase }
54
+ return agent_comments.any? unless since
55
+
56
+ agent_comments.any? do |c|
57
+ comment_time = c["created_at"] && Time.parse(c["created_at"])
58
+ comment_time && comment_time > since
59
+ end
60
+ rescue StandardError
61
+ false
62
+ end
63
+
64
+ # Post a minimal server-generated comment when all else fails.
65
+ def post_fallback_comment(card_number, branch, agent_name, project_config)
66
+ repo_path = project_config["repo_path"]
67
+ env = Helpers.fizzy_env_for(agent_name)
68
+
69
+ pr_url = Helpers.send(:detect_pr_url, branch, project_config)
70
+ body = "<p>✅ Work completed on this card.</p>"
71
+ body += "<p><a href=\"#{pr_url}\">View PR</a></p>" if pr_url
72
+ body += "<p><strong>Branch:</strong> <code>#{branch}</code></p>"
73
+
74
+ run_cmd("fizzy", "comment", "create", "--card", card_number.to_s, "--body", body,
75
+ chdir: repo_path, env: env)
76
+ LOG.info "[Fizzy] Posted fallback comment on card ##{card_number}" if defined?(LOG)
77
+ rescue StandardError => e
78
+ LOG.error "[Fizzy] Failed to post fallback comment on card ##{card_number}: #{e.message}" if defined?(LOG)
79
+ end
80
+
81
+ private
82
+
83
+ def find_work_item(map, card_number)
84
+ map.values.find do |info|
85
+ info.is_a?(Hash) &&
86
+ (info.dig("sources", "fizzy", "card_number").to_s == card_number.to_s ||
87
+ info["number"].to_s == card_number.to_s)
88
+ end
89
+ end
90
+
91
+ def dispatch_summarize_agent(card_number, card_title, branch, agent_name, project_config, worktree_path)
92
+ prompt = render_prompt(Prompts::SUMMARIZE_WORK,
93
+ { "CARD_NUMBER" => card_number, "CARD_TITLE" => card_title, "BRANCH" => branch },
94
+ brain_context: "", agent_name: agent_name, channel: :fizzy,
95
+ board_key: Config.board_key_for_project(project_config))
96
+
97
+ run_agent(prompt,
98
+ project_config: project_config, chdir: worktree_path,
99
+ log_name: "summarize-#{card_number}", agent_name: agent_name,
100
+ source: :fizzy,
101
+ source_context: { card_number: card_number, skip_summarize_redispatch: true },
102
+ skip_column_move: true)
103
+ end
104
+
105
+ def monitor_summarize_session(pid, card_number, agent_name, branch, repo_path, session_started_at,
106
+ project_config)
107
+ Thread.new do
108
+ wait_for_process_or_timeout(pid, timeout: 60)
109
+ sleep 2 # Brief pause to let any comment propagate
110
+
111
+ unless agent_commented_on_card?(card_number, agent_name, repo_path: repo_path, since: session_started_at)
112
+ post_fallback_comment(card_number, branch, agent_name, project_config)
113
+ end
114
+ rescue Errno::ECHILD
115
+ # Process already reaped by session manager — check for comment
116
+ sleep 2
117
+ unless agent_commented_on_card?(card_number, agent_name, repo_path: repo_path, since: session_started_at)
118
+ post_fallback_comment(card_number, branch, agent_name, project_config)
119
+ end
120
+ rescue StandardError => e
121
+ LOG.error "[Fizzy] Summarize monitor error for card ##{card_number}: #{e.message}" if defined?(LOG)
122
+ end
123
+ end
124
+
125
+ def wait_for_process_or_timeout(pid, timeout: 60)
126
+ deadline = Time.now + timeout
127
+ loop do
128
+ result = Process.waitpid2(pid, Process::WNOHANG)
129
+ break if result
130
+
131
+ if Time.now > deadline
132
+ safe_kill(pid)
133
+ break
134
+ end
135
+ sleep 1
136
+ end
137
+ end
138
+
139
+ def safe_kill(pid)
140
+ Process.kill("TERM", pid)
141
+ Process.waitpid2(pid)
142
+ rescue Errno::ESRCH, Errno::ECHILD
143
+ # Process already gone
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -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
@@ -43,6 +46,16 @@ module Brainiac
43
46
  project_config: ctx[:project_config],
44
47
  agent_name: ctx[:agent_name])
45
48
 
49
+ # If the agent didn't post any comment during this session, re-dispatch to summarize from memory.
50
+ # Uses dispatched_at from source_context to only check for comments made during THIS session,
51
+ # not previous sessions on the same card.
52
+ if !ctx[:source_context]&.dig(:skip_summarize_redispatch) &&
53
+ !agent_commented_on_card?(card_number, ctx[:agent_name],
54
+ repo_path: ctx[:project_config]["repo_path"],
55
+ since: ctx[:source_context]&.dig(:dispatched_at))
56
+ dispatch_summarize_session(ctx)
57
+ end
58
+
46
59
  # Planning mode finalization
47
60
  Planning.finalize_if_needed(ctx[:prompt_file], ctx[:agent_name], ctx[:project_config])
48
61
  end
@@ -240,13 +253,28 @@ module Brainiac
240
253
 
241
254
  pid, log_file = run_agent(prompt, project_config: project_config, chdir: repo_path,
242
255
  log_name: "uat-#{card_number}", agent_name: agent_name,
243
- source: :fizzy, source_context: { card_number: card_number }, skip_column_move: true)
256
+ source: :fizzy,
257
+ source_context: { card_number: card_number, dispatched_at: Time.now },
258
+ skip_column_move: true)
244
259
  register_session("card-#{card_number}", pid, log_file: log_file, agent_name: agent_name)
245
260
  LOG.info "[Fizzy] Dispatched #{agent_name} for UAT on card ##{card_number}" if defined?(LOG)
246
261
  rescue StandardError => e
247
262
  LOG.error "[Fizzy] Failed to dispatch UAT agent: #{e.message}" if defined?(LOG)
248
263
  end
249
264
 
265
+ # Delegated to Summarize module for manageable file length.
266
+ def dispatch_summarize_session(ctx)
267
+ Summarize.dispatch_summarize_session(ctx)
268
+ end
269
+
270
+ def agent_commented_on_card?(card_number, agent_name, repo_path:, since: nil)
271
+ Summarize.agent_commented_on_card?(card_number, agent_name, repo_path: repo_path, since: since)
272
+ end
273
+
274
+ def post_fallback_comment(card_number, branch, agent_name, project_config)
275
+ Summarize.post_fallback_comment(card_number, branch, agent_name, project_config)
276
+ end
277
+
250
278
  def close_uat_cards(card_list, repo_path)
251
279
  closed = []
252
280
  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.12"
6
+ VERSION = "0.0.14"
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.12
4
+ version: 0.0.14
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