@@ -114,6 +114,19 @@ def handle_stop(data: dict):
114114 except Exception :
115115 pass # Never block session stop
116116
117+ # Impact report: render session impact (#1064)
118+ try :
119+ duration_secs = final_data .get ("duration_seconds" , 0 )
120+ if duration_secs >= 30 :
121+ impact_dir = os .environ .get (
122+ "CLAUDE_PROJECT_DIR" , os .getcwd ()
123+ )
124+ report = _render_impact_report (session_id , impact_dir )
125+ if report :
126+ summary += "\n \n " + report
127+ except Exception :
128+ pass # Never block session stop
129+
117130 # Agent memory: record session agent activity (#947)
118131 try :
119132 from agent_memory import AgentMemory
@@ -179,6 +192,106 @@ def handle_stop(data: dict):
179192 return None
180193
181194
195+ def _render_impact_report (session_id , project_dir ):
196+ """Render impact report from JSONL events for the given session (#1064).
197+
198+ Reads impact-events.jsonl directly (MCP server may already be stopping).
199+ Returns empty string if no events found.
200+ """
201+ jsonl_path = os .path .join (
202+ project_dir , "docs" , "codingbuddy" , "impact-events.jsonl"
203+ )
204+ if not os .path .isfile (jsonl_path ):
205+ return ""
206+
207+ events = []
208+ with open (jsonl_path , "r" , encoding = "utf-8" ) as f :
209+ for line in f :
210+ line = line .strip ()
211+ if not line :
212+ continue
213+ try :
214+ evt = json .loads (line )
215+ if evt .get ("sessionId" ) == session_id :
216+ events .append (evt )
217+ except (json .JSONDecodeError , KeyError ):
218+ continue
219+
220+ if not events :
221+ return ""
222+
223+ # Aggregate
224+ issues_prevented = 0
225+ issues_by_domain = {}
226+ agents_dispatched = 0
227+ checklists_generated = 0
228+ checklist_domains = set ()
229+ mode_transitions = []
230+
231+ for evt in events :
232+ et = evt .get ("eventType" , "" )
233+ data = evt .get ("data" , {})
234+
235+ if et in ("issue_found" , "issue_prevented" ):
236+ count = data .get ("count" ) or 1
237+ issues_prevented += count
238+ domain = data .get ("domain" )
239+ if domain :
240+ issues_by_domain [domain ] = (
241+ issues_by_domain .get (domain , 0 ) + count
242+ )
243+ elif et == "agent_dispatched" :
244+ agents_dispatched += 1
245+ elif et == "checklist_generated" :
246+ checklists_generated += 1
247+ domain = data .get ("domain" )
248+ if domain :
249+ checklist_domains .add (domain )
250+ elif et == "mode_activated" :
251+ mode = data .get ("mode" )
252+ if mode :
253+ mode_transitions .append (mode )
254+
255+ # Build content rows
256+ rows = []
257+ if issues_prevented > 0 :
258+ rows .append (f" 🛡️ Issues prevented { issues_prevented } " )
259+ domain_parts = " " .join (
260+ f"• { d } : { c } " for d , c in issues_by_domain .items ()
261+ )
262+ if domain_parts :
263+ rows .append (f" { domain_parts } " )
264+ if agents_dispatched > 0 :
265+ rows .append (
266+ f" 🤖 Agents dispatched { agents_dispatched } specialists"
267+ )
268+ if checklists_generated > 0 :
269+ rows .append (
270+ f" 📋 Checklists applied { len (checklist_domains )} domains"
271+ )
272+ if mode_transitions :
273+ rows .append (
274+ f" 🔄 Mode transitions { '→' .join (mode_transitions )} "
275+ )
276+
277+ if not rows :
278+ return ""
279+
280+ # Box rendering
281+ BOX_W = 41
282+ hr = "─" * BOX_W
283+
284+ def _box (text ):
285+ return f"│{ text .ljust (BOX_W )} │"
286+
287+ lines = [f"╭{ hr } ╮" , _box (" 📊 Impact Report" ), f"├{ hr } ┤" ]
288+ for row in rows :
289+ lines .append (_box (row ))
290+ lines .append (f"╰{ hr } ╯" )
291+
292+ return "\n " .join (lines )
293+
294+
182295def _maybe_notify_session_end (summary : str ):
183296 """Send session summary notification if configured."""
184297 if not summary :
0 commit comments