yeaboi 2.8.0__py3-none-any.whl

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.
Files changed (153) hide show
  1. yeaboi/__init__.py +74 -0
  2. yeaboi/agent/__init__.py +73 -0
  3. yeaboi/agent/ceremony_history.py +255 -0
  4. yeaboi/agent/graph.py +296 -0
  5. yeaboi/agent/llm.py +231 -0
  6. yeaboi/agent/nodes.py +8367 -0
  7. yeaboi/agent/repo_signals.py +504 -0
  8. yeaboi/agent/state.py +993 -0
  9. yeaboi/azdevops_sync.py +692 -0
  10. yeaboi/cli.py +1301 -0
  11. yeaboi/config.py +519 -0
  12. yeaboi/formatters.py +637 -0
  13. yeaboi/html_exporter.py +827 -0
  14. yeaboi/input_guardrails.py +354 -0
  15. yeaboi/jira_sync.py +714 -0
  16. yeaboi/json_exporter.py +115 -0
  17. yeaboi/music.py +353 -0
  18. yeaboi/output_guardrails.py +239 -0
  19. yeaboi/paths.py +334 -0
  20. yeaboi/performance/__init__.py +48 -0
  21. yeaboi/performance/activity.py +140 -0
  22. yeaboi/performance/context.py +101 -0
  23. yeaboi/performance/delivery.py +83 -0
  24. yeaboi/performance/engine.py +483 -0
  25. yeaboi/performance/export.py +203 -0
  26. yeaboi/performance/references/competency_framework.md +42 -0
  27. yeaboi/performance/render.py +189 -0
  28. yeaboi/performance/roster.py +108 -0
  29. yeaboi/performance/store.py +359 -0
  30. yeaboi/persistence.py +959 -0
  31. yeaboi/prompts/__init__.py +27 -0
  32. yeaboi/prompts/analyzer.py +267 -0
  33. yeaboi/prompts/feature_generator.py +187 -0
  34. yeaboi/prompts/intake.py +638 -0
  35. yeaboi/prompts/performance.py +201 -0
  36. yeaboi/prompts/reporting.py +88 -0
  37. yeaboi/prompts/retro.py +64 -0
  38. yeaboi/prompts/sprint_planner.py +242 -0
  39. yeaboi/prompts/standup.py +76 -0
  40. yeaboi/prompts/story_writer.py +317 -0
  41. yeaboi/prompts/system.py +107 -0
  42. yeaboi/prompts/task_decomposer.py +211 -0
  43. yeaboi/questionnaire_io.py +196 -0
  44. yeaboi/repl/__init__.py +1316 -0
  45. yeaboi/repl/_intake_menu.py +93 -0
  46. yeaboi/repl/_io.py +573 -0
  47. yeaboi/repl/_mode_menu.py +66 -0
  48. yeaboi/repl/_questionnaire.py +396 -0
  49. yeaboi/repl/_review.py +162 -0
  50. yeaboi/repl/_ui.py +177 -0
  51. yeaboi/reporting/__init__.py +55 -0
  52. yeaboi/reporting/activity.py +165 -0
  53. yeaboi/reporting/engine.py +375 -0
  54. yeaboi/reporting/export.py +192 -0
  55. yeaboi/reporting/presentation.py +285 -0
  56. yeaboi/reporting/render.py +114 -0
  57. yeaboi/reporting/sprints.py +163 -0
  58. yeaboi/reporting/store.py +196 -0
  59. yeaboi/retro/__init__.py +29 -0
  60. yeaboi/retro/board.py +428 -0
  61. yeaboi/retro/engine.py +112 -0
  62. yeaboi/retro/export.py +164 -0
  63. yeaboi/retro/page.py +856 -0
  64. yeaboi/retro/server.py +392 -0
  65. yeaboi/retro/store.py +212 -0
  66. yeaboi/retro/tunnel.py +229 -0
  67. yeaboi/sessions.py +767 -0
  68. yeaboi/setup_wizard.py +288 -0
  69. yeaboi/skills/__init__.py +0 -0
  70. yeaboi/skills/scrum-planner/README.md +159 -0
  71. yeaboi/skills/scrum-planner/SKILL.md +338 -0
  72. yeaboi/skills/scrum-planner/references/cli-and-generation.md +85 -0
  73. yeaboi/skills/scrum-planner/references/output-and-review.md +125 -0
  74. yeaboi/skills/scrum-planner/scripts/canvas.py +233 -0
  75. yeaboi/skills/scrum-planner/scripts/canvas_push.py +231 -0
  76. yeaboi/standup/__init__.py +26 -0
  77. yeaboi/standup/collector.py +203 -0
  78. yeaboi/standup/confidence.py +171 -0
  79. yeaboi/standup/delivery.py +229 -0
  80. yeaboi/standup/engine.py +358 -0
  81. yeaboi/standup/errors.py +26 -0
  82. yeaboi/standup/export.py +201 -0
  83. yeaboi/standup/interactive.py +119 -0
  84. yeaboi/standup/render.py +125 -0
  85. yeaboi/standup/scheduler.py +297 -0
  86. yeaboi/standup/sprint_context.py +104 -0
  87. yeaboi/standup/store.py +356 -0
  88. yeaboi/team_profile.py +687 -0
  89. yeaboi/team_profile_exporter.py +2237 -0
  90. yeaboi/telemetry.py +256 -0
  91. yeaboi/tools/__init__.py +135 -0
  92. yeaboi/tools/azure_devops.py +1015 -0
  93. yeaboi/tools/calendar_tools.py +308 -0
  94. yeaboi/tools/codebase.py +559 -0
  95. yeaboi/tools/confluence.py +439 -0
  96. yeaboi/tools/github.py +434 -0
  97. yeaboi/tools/jira.py +835 -0
  98. yeaboi/tools/llm_tools.py +158 -0
  99. yeaboi/tools/local_git.py +83 -0
  100. yeaboi/tools/notion.py +484 -0
  101. yeaboi/tools/team_learning.py +5421 -0
  102. yeaboi/ui/__init__.py +2 -0
  103. yeaboi/ui/mode_select/__init__.py +4967 -0
  104. yeaboi/ui/mode_select/screens/__init__.py +1 -0
  105. yeaboi/ui/mode_select/screens/_project_cards.py +588 -0
  106. yeaboi/ui/mode_select/screens/_project_list_screen.py +770 -0
  107. yeaboi/ui/mode_select/screens/_screens.py +381 -0
  108. yeaboi/ui/mode_select/screens/_screens_secondary.py +4373 -0
  109. yeaboi/ui/provider_select/__init__.py +893 -0
  110. yeaboi/ui/provider_select/_config.py +41 -0
  111. yeaboi/ui/provider_select/_constants.py +237 -0
  112. yeaboi/ui/provider_select/_nav.py +63 -0
  113. yeaboi/ui/provider_select/_phase_confluence.py +261 -0
  114. yeaboi/ui/provider_select/_phase_issue_tracking.py +387 -0
  115. yeaboi/ui/provider_select/_phase_notion.py +241 -0
  116. yeaboi/ui/provider_select/_transitions.py +95 -0
  117. yeaboi/ui/provider_select/_verification.py +508 -0
  118. yeaboi/ui/provider_select/screens/__init__.py +1 -0
  119. yeaboi/ui/provider_select/screens/_screens.py +565 -0
  120. yeaboi/ui/provider_select/screens/_screens_vc.py +378 -0
  121. yeaboi/ui/session/__init__.py +389 -0
  122. yeaboi/ui/session/_dry_run.py +98 -0
  123. yeaboi/ui/session/_renderers.py +1179 -0
  124. yeaboi/ui/session/_utils.py +394 -0
  125. yeaboi/ui/session/editor/__init__.py +1 -0
  126. yeaboi/ui/session/editor/_editor.py +717 -0
  127. yeaboi/ui/session/editor/_editor_artifacts.py +529 -0
  128. yeaboi/ui/session/editor/_editor_core.py +457 -0
  129. yeaboi/ui/session/phases/__init__.py +1 -0
  130. yeaboi/ui/session/phases/_phases.py +1685 -0
  131. yeaboi/ui/session/phases/_phases_intake.py +659 -0
  132. yeaboi/ui/session/phases/_phases_review.py +323 -0
  133. yeaboi/ui/session/screens/__init__.py +1 -0
  134. yeaboi/ui/session/screens/_accordion.py +508 -0
  135. yeaboi/ui/session/screens/_screens.py +209 -0
  136. yeaboi/ui/session/screens/_screens_input.py +386 -0
  137. yeaboi/ui/session/screens/_screens_pipeline.py +543 -0
  138. yeaboi/ui/shared/__init__.py +43 -0
  139. yeaboi/ui/shared/_animations.py +164 -0
  140. yeaboi/ui/shared/_ascii_font.py +67 -0
  141. yeaboi/ui/shared/_components.py +378 -0
  142. yeaboi/ui/shared/_input.py +313 -0
  143. yeaboi/ui/shared/_music_bar.py +159 -0
  144. yeaboi/ui/shared/_tips.py +107 -0
  145. yeaboi/ui/shared/_voice_input.py +207 -0
  146. yeaboi/ui/shared/_wordmarks.py +150 -0
  147. yeaboi/ui/splash.py +331 -0
  148. yeaboi/voice.py +196 -0
  149. yeaboi-2.8.0.dist-info/METADATA +2344 -0
  150. yeaboi-2.8.0.dist-info/RECORD +153 -0
  151. yeaboi-2.8.0.dist-info/WHEEL +4 -0
  152. yeaboi-2.8.0.dist-info/entry_points.txt +3 -0
  153. yeaboi-2.8.0.dist-info/licenses/LICENSE +21 -0
yeaboi/__init__.py ADDED
@@ -0,0 +1,74 @@
1
+ """yeaboi.ai — a team lead's best friend for project planning and delivery."""
2
+
3
+ import logging
4
+ import warnings
5
+ from importlib.metadata import PackageNotFoundError
6
+ from importlib.metadata import version as _pkg_version
7
+
8
+ # Suppress LangChain's Pydantic V1 compatibility warning on Python 3.14+.
9
+ # langchain-core internally imports pydantic.v1 shims which don't work on
10
+ # Python 3.14. The warning is non-fatal — the agent runs correctly.
11
+ # Remove once langchain-core fully migrates off pydantic.v1.
12
+ warnings.filterwarnings("ignore", message="Core Pydantic V1 functionality", category=UserWarning)
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Suppress LangSmith 429 rate-limit errors from appearing in the terminal.
17
+ #
18
+ # LangSmith's background trace uploader logs errors via the 'langsmith' Python
19
+ # logger. When the free-tier rate limit is hit, those messages interrupt the
20
+ # REPL output with noise the user can't act on.
21
+ #
22
+ # Fix: attach a filter + silent handler to logging.getLogger("langsmith") and
23
+ # set propagate=False so messages never reach the root StreamHandler.
24
+ #
25
+ # The filter also calls disable_langsmith_tracing() on the first 429 hit so
26
+ # the uploader stops retrying for the rest of the process.
27
+ #
28
+ # Why _SilentHandler instead of NullHandler? NullHandler.handle() overrides
29
+ # the base and skips filter checks entirely — so a filter attached to it would
30
+ # never run. _SilentHandler inherits the base Handler.handle() which calls
31
+ # self.filter() before emit(), giving us the interception point we need.
32
+ #
33
+ # # See README: "Architecture" — the CLI layer manages all user-facing chrome,
34
+ # # including suppressing third-party noise that would confuse the user.
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ class _LangSmithRateLimitFilter(logging.Filter):
39
+ """Block LangSmith 429 messages and auto-disable tracing when rate-limited."""
40
+
41
+ def filter(self, record: logging.LogRecord) -> bool:
42
+ msg = record.getMessage()
43
+ if "429" in msg or "Rate limit" in msg or "LangSmithRateLimitError" in msg:
44
+ # Lazy import — config may not be importable in all test contexts.
45
+ try:
46
+ from yeaboi.config import disable_langsmith_tracing
47
+
48
+ disable_langsmith_tracing()
49
+ except Exception:
50
+ pass
51
+ return False # suppress this log record
52
+ return True
53
+
54
+
55
+ class _SilentHandler(logging.Handler):
56
+ """No-op handler; discards all records. Present so propagate=False doesn't
57
+ trigger Python's "No handlers found" last-resort warning."""
58
+
59
+ def emit(self, record: logging.LogRecord) -> None:
60
+ pass
61
+
62
+
63
+ _ls_handler = _SilentHandler()
64
+ _ls_handler.addFilter(_LangSmithRateLimitFilter())
65
+ _ls_logger = logging.getLogger("langsmith")
66
+ _ls_logger.addHandler(_ls_handler)
67
+ _ls_logger.propagate = False # stop records reaching the root StreamHandler
68
+
69
+ # Single source of truth: the version lives only in pyproject.toml and is read
70
+ # here from the installed package metadata. Bump pyproject.toml to release.
71
+ try:
72
+ __version__ = _pkg_version("yeaboi")
73
+ except PackageNotFoundError: # running from a raw source tree without an install
74
+ __version__ = "0.0.0+dev"
@@ -0,0 +1,73 @@
1
+ """Agent graph module."""
2
+
3
+ from yeaboi.agent.graph import create_graph
4
+ from yeaboi.agent.llm import get_llm
5
+ from yeaboi.agent.nodes import (
6
+ call_model,
7
+ feature_generator,
8
+ feature_skip,
9
+ human_review,
10
+ make_call_model,
11
+ project_analyzer,
12
+ project_intake,
13
+ resolve_sprint_selection,
14
+ route_entry,
15
+ should_continue,
16
+ sprint_planner,
17
+ story_writer,
18
+ task_decomposer,
19
+ )
20
+ from yeaboi.agent.state import (
21
+ DeliveredItem,
22
+ DeliveryReport,
23
+ Discipline,
24
+ EngineerActivity,
25
+ EngineerRef,
26
+ EngineerStory,
27
+ MemberUpdate,
28
+ OneOnOnePrep,
29
+ OneOnOneRecord,
30
+ ProjectAnalysis,
31
+ PromptQualityRating,
32
+ RetroCard,
33
+ RetroReport,
34
+ ScrumState,
35
+ SixMonthReview,
36
+ StandupReport,
37
+ )
38
+ from yeaboi.prompts.intake import AnswerSource
39
+
40
+ __all__ = [
41
+ "AnswerSource",
42
+ "DeliveredItem",
43
+ "DeliveryReport",
44
+ "Discipline",
45
+ "EngineerActivity",
46
+ "EngineerRef",
47
+ "EngineerStory",
48
+ "MemberUpdate",
49
+ "OneOnOnePrep",
50
+ "OneOnOneRecord",
51
+ "ProjectAnalysis",
52
+ "PromptQualityRating",
53
+ "RetroCard",
54
+ "RetroReport",
55
+ "ScrumState",
56
+ "SixMonthReview",
57
+ "StandupReport",
58
+ "call_model",
59
+ "create_graph",
60
+ "feature_generator",
61
+ "feature_skip",
62
+ "get_llm",
63
+ "human_review",
64
+ "make_call_model",
65
+ "project_analyzer",
66
+ "project_intake",
67
+ "route_entry",
68
+ "resolve_sprint_selection",
69
+ "should_continue",
70
+ "sprint_planner",
71
+ "story_writer",
72
+ "task_decomposer",
73
+ ]
@@ -0,0 +1,255 @@
1
+ """Team ceremony history (Daily Standup + Retro) for Planning & Analysis.
2
+
3
+ # See README: "Session Management" — SQLite persistence
4
+ # See README: "Prompt Construction" — ARC framework (optional context sections)
5
+
6
+ Standup and Retro modes each persist a report per run to the shared
7
+ ``~/.scrum-agent/sessions.db``. This module reads that history back **team-wide**
8
+ (across all sessions, retros prioritised project-first) and distils it into
9
+ deterministic signals the Planning analyzer / sprint planner and the Analysis
10
+ report can consume:
11
+
12
+ - **action_items** — deduped, unresolved retro action-item card texts (newest wins),
13
+ fed to the story writer to seed the backlog (badged ``[Retro]``).
14
+ - **themes** — recurring "what went well" / "what didn't go well" topics.
15
+ - **cadence** — how often retros / standups actually run (from run timestamps).
16
+ - **confidence_trend** — average sprint-confidence from standups and its direction.
17
+
18
+ Design mirrors ``agent/repo_signals.py``: a graceful I/O entry point
19
+ (``gather_ceremony_context``) that never raises, plus pure helpers that are
20
+ trivially unit-testable. Cadence is derived from the *intervals between runs*, so
21
+ it needs no notion of "now" and is fully deterministic.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from collections import Counter
28
+ from dataclasses import dataclass
29
+ from datetime import datetime
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Minimum times a normalised card text must recur to count as a "theme".
34
+ _THEME_MIN_COUNT = 2
35
+ # How many themes to surface per grid.
36
+ _MAX_THEMES = 5
37
+
38
+
39
+ @dataclass
40
+ class CeremonyContext:
41
+ """Distilled, deterministic view of the team's recent standups + retros.
42
+
43
+ Transient — never persisted. ``summary_md`` is the block injected into the
44
+ planning prompts; ``action_items`` seed the backlog; the rest drives the
45
+ Analysis "Ceremony Cadence & Trends" section.
46
+ """
47
+
48
+ summary_md: str = ""
49
+ action_items: tuple[str, ...] = ()
50
+ retro_count: int = 0
51
+ standup_count: int = 0
52
+ retro_cadence: str = ""
53
+ standup_cadence: str = ""
54
+ confidence_trend: str = ""
55
+ went_well_themes: tuple[tuple[str, int], ...] = ()
56
+ didnt_go_well_themes: tuple[tuple[str, int], ...] = ()
57
+
58
+ @property
59
+ def is_empty(self) -> bool:
60
+ return self.retro_count == 0 and self.standup_count == 0
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Pure helpers — no I/O, deterministic (no dependence on the current time).
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ def _normalise(text: str) -> str:
69
+ """Lowercase + collapse whitespace so near-identical cards group together."""
70
+ return " ".join((text or "").lower().split())
71
+
72
+
73
+ def _parse_ts(value: str) -> datetime | None:
74
+ """Parse an ISO-8601 timestamp, tolerating a trailing 'Z'."""
75
+ try:
76
+ return datetime.fromisoformat((value or "").replace("Z", "+00:00"))
77
+ except (ValueError, TypeError):
78
+ return None
79
+
80
+
81
+ def _avg_interval_days(run_ats: list[str]) -> float | None:
82
+ """Average gap in days between consecutive runs (order-independent).
83
+
84
+ Returns None when fewer than two parseable timestamps are present.
85
+ """
86
+ stamps = sorted(ts for ts in (_parse_ts(r) for r in run_ats) if ts is not None)
87
+ if len(stamps) < 2:
88
+ return None
89
+ gaps = [(b - a).total_seconds() / 86400.0 for a, b in zip(stamps, stamps[1:], strict=False)]
90
+ return sum(gaps) / len(gaps) if gaps else None
91
+
92
+
93
+ def _describe_cadence(run_ats: list[str], noun: str) -> str:
94
+ """Human phrase for how often something runs (e.g. "~every 14 days over 4 runs")."""
95
+ count = len([r for r in run_ats if _parse_ts(r) is not None])
96
+ if count == 0:
97
+ return f"no {noun}s recorded"
98
+ if count == 1:
99
+ return f"1 {noun} recorded (not yet a cadence)"
100
+ avg = _avg_interval_days(run_ats)
101
+ if avg is None:
102
+ return f"{count} {noun}s recorded"
103
+ if avg < 1:
104
+ freq = "roughly daily"
105
+ elif avg <= 10:
106
+ freq = f"~every {round(avg)} day(s)"
107
+ else:
108
+ freq = f"~every {round(avg / 7)} week(s)"
109
+ return f"{freq} ({count} {noun}s)"
110
+
111
+
112
+ def _confidence_trend(history: list[dict]) -> tuple[str, int | None]:
113
+ """Average sprint-confidence + direction from standup history (newest first).
114
+
115
+ Compares the recent half to the older half. Returns (phrase, avg_pct) — or
116
+ ("", None) when there isn't enough data.
117
+ """
118
+ pcts = [int(h.get("confidence_pct", 0)) for h in history if h.get("status", "success") == "success"]
119
+ pcts = [p for p in pcts if p > 0]
120
+ if not pcts:
121
+ return "", None
122
+ avg = round(sum(pcts) / len(pcts))
123
+ if len(pcts) < 4:
124
+ return f"{avg}% average confidence", avg
125
+ # history is newest-first: the first half is the recent window.
126
+ mid = len(pcts) // 2
127
+ recent = sum(pcts[:mid]) / mid
128
+ older = sum(pcts[mid:]) / (len(pcts) - mid)
129
+ delta = recent - older
130
+ direction = "improving" if delta >= 5 else ("declining" if delta <= -5 else "flat")
131
+ return f"{avg}% average confidence, {direction}", avg
132
+
133
+
134
+ def _top_themes(reports, grid: str) -> tuple[tuple[str, int], ...]:
135
+ """Most frequent (normalised) card texts in a grid across reports.
136
+
137
+ Returns (representative_text, count) pairs for texts recurring at least
138
+ ``_THEME_MIN_COUNT`` times, most frequent first, capped at ``_MAX_THEMES``.
139
+ """
140
+ counts: Counter[str] = Counter()
141
+ representative: dict[str, str] = {}
142
+ for report in reports:
143
+ for card in report.cards:
144
+ if card.grid != grid or not card.text.strip():
145
+ continue
146
+ key = _normalise(card.text)
147
+ counts[key] += 1
148
+ representative.setdefault(key, card.text.strip())
149
+ themes = [(representative[k], n) for k, n in counts.most_common() if n >= _THEME_MIN_COUNT]
150
+ return tuple(themes[:_MAX_THEMES])
151
+
152
+
153
+ def _dedup_action_items(reports) -> tuple[str, ...]:
154
+ """Distinct action-item card texts across reports, newest-report first.
155
+
156
+ ``reports`` is newest-first; the first occurrence of each normalised text
157
+ wins so the freshest wording is kept. AI-suggested and human cards both count.
158
+ """
159
+ seen: set[str] = set()
160
+ out: list[str] = []
161
+ for report in reports:
162
+ for card in report.cards:
163
+ if card.grid != "action_items":
164
+ continue
165
+ text = card.text.strip()
166
+ key = _normalise(text)
167
+ if not key or key in seen:
168
+ continue
169
+ seen.add(key)
170
+ out.append(text)
171
+ return tuple(out)
172
+
173
+
174
+ def _bullets(items) -> str:
175
+ return "\n".join(f"- {it}" for it in items)
176
+
177
+
178
+ def format_ceremony_history_md(ctx: CeremonyContext) -> str:
179
+ """Render a CeremonyContext into the markdown block injected into prompts."""
180
+ if ctx.is_empty:
181
+ return ""
182
+ parts: list[str] = []
183
+ if ctx.action_items:
184
+ parts.append("**Open retro action items:**\n" + _bullets(ctx.action_items))
185
+ if ctx.didnt_go_well_themes:
186
+ parts.append(
187
+ "**Recurring pain points (retro 'didn't go well'):**\n"
188
+ + _bullets(f"{t} ({n}×)" for t, n in ctx.didnt_go_well_themes)
189
+ )
190
+ if ctx.went_well_themes:
191
+ parts.append(
192
+ "**What's been working (retro 'went well'):**\n" + _bullets(f"{t} ({n}×)" for t, n in ctx.went_well_themes)
193
+ )
194
+ if ctx.confidence_trend:
195
+ parts.append(f"**Recent standup confidence:** {ctx.confidence_trend}.")
196
+ cadence_bits = [b for b in (ctx.retro_cadence, ctx.standup_cadence) if b]
197
+ if cadence_bits:
198
+ parts.append("**Cadence:** retros " + ctx.retro_cadence + "; standups " + ctx.standup_cadence + ".")
199
+ return "\n\n".join(parts)
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # I/O entry point — graceful (never raises); mirrors repo_signals.scan_*.
204
+ # ---------------------------------------------------------------------------
205
+
206
+
207
+ def gather_ceremony_context(
208
+ project_name: str = "", *, retro_limit: int = 5, standup_limit: int = 10
209
+ ) -> CeremonyContext:
210
+ """Read the team's recent retros + standups and distil them (team-wide).
211
+
212
+ Retros are fetched project-first (matching ``project_name`` sort ahead of
213
+ others); standups are recency-based (their table has no project column).
214
+ Graceful: a missing DB / empty tables / any error yields an empty context —
215
+ planning/analysis then behave exactly as before.
216
+
217
+ # See README: "Session Management" — SQLite persistence
218
+ """
219
+ try:
220
+ from yeaboi.config import get_sessions_db
221
+ from yeaboi.retro.store import RetroStore
222
+ from yeaboi.standup.store import StandupStore
223
+
224
+ db_path = get_sessions_db()
225
+ if not db_path.exists():
226
+ return CeremonyContext()
227
+
228
+ with RetroStore(db_path) as rstore:
229
+ retros = rstore.get_recent_reports(retro_limit, project_name)
230
+ retro_hist = rstore.get_all_history(100)
231
+ with StandupStore(db_path) as sstore:
232
+ standups = sstore.get_recent_reports(standup_limit)
233
+ standup_hist = sstore.get_all_history(100)
234
+ except Exception: # noqa: BLE001 — ceremony history is best-effort; never abort a plan
235
+ logger.debug("gather_ceremony_context failed (non-fatal)", exc_info=True)
236
+ return CeremonyContext()
237
+
238
+ ctx = CeremonyContext(
239
+ action_items=_dedup_action_items(retros),
240
+ retro_count=len(retros),
241
+ standup_count=len(standups),
242
+ retro_cadence=_describe_cadence([h["run_at"] for h in retro_hist], "retro"),
243
+ standup_cadence=_describe_cadence([h["run_at"] for h in standup_hist], "standup"),
244
+ confidence_trend=_confidence_trend(standup_hist)[0],
245
+ went_well_themes=_top_themes(retros, "went_well"),
246
+ didnt_go_well_themes=_top_themes(retros, "didnt_go_well"),
247
+ )
248
+ ctx.summary_md = format_ceremony_history_md(ctx)
249
+ logger.info(
250
+ "ceremony_history: %d retro(s), %d standup(s), %d action item(s)",
251
+ ctx.retro_count,
252
+ ctx.standup_count,
253
+ len(ctx.action_items),
254
+ )
255
+ return ctx