Skip to content

Commit 06d3447

Browse files
committed
feat(plugin): render session impact report in stop hook
Add _render_impact_report() that reads impact-events.jsonl directly (MCP server may already be stopping) and renders a box-formatted summary of issues prevented, agents dispatched, checklists applied, and mode transitions. Only shown for sessions >= 30s with at least one impact event. All errors wrapped in try-except to never block session stop. Closes #1064
1 parent d5cef11 commit 06d3447

1 file changed

Lines changed: 113 additions & 0 deletions

File tree

  • packages/claude-code-plugin/hooks

packages/claude-code-plugin/hooks/stop.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
182295
def _maybe_notify_session_end(summary: str):
183296
"""Send session summary notification if configured."""
184297
if not summary:

0 commit comments

Comments
 (0)