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.
- yeaboi/__init__.py +74 -0
- yeaboi/agent/__init__.py +73 -0
- yeaboi/agent/ceremony_history.py +255 -0
- yeaboi/agent/graph.py +296 -0
- yeaboi/agent/llm.py +231 -0
- yeaboi/agent/nodes.py +8367 -0
- yeaboi/agent/repo_signals.py +504 -0
- yeaboi/agent/state.py +993 -0
- yeaboi/azdevops_sync.py +692 -0
- yeaboi/cli.py +1301 -0
- yeaboi/config.py +519 -0
- yeaboi/formatters.py +637 -0
- yeaboi/html_exporter.py +827 -0
- yeaboi/input_guardrails.py +354 -0
- yeaboi/jira_sync.py +714 -0
- yeaboi/json_exporter.py +115 -0
- yeaboi/music.py +353 -0
- yeaboi/output_guardrails.py +239 -0
- yeaboi/paths.py +334 -0
- yeaboi/performance/__init__.py +48 -0
- yeaboi/performance/activity.py +140 -0
- yeaboi/performance/context.py +101 -0
- yeaboi/performance/delivery.py +83 -0
- yeaboi/performance/engine.py +483 -0
- yeaboi/performance/export.py +203 -0
- yeaboi/performance/references/competency_framework.md +42 -0
- yeaboi/performance/render.py +189 -0
- yeaboi/performance/roster.py +108 -0
- yeaboi/performance/store.py +359 -0
- yeaboi/persistence.py +959 -0
- yeaboi/prompts/__init__.py +27 -0
- yeaboi/prompts/analyzer.py +267 -0
- yeaboi/prompts/feature_generator.py +187 -0
- yeaboi/prompts/intake.py +638 -0
- yeaboi/prompts/performance.py +201 -0
- yeaboi/prompts/reporting.py +88 -0
- yeaboi/prompts/retro.py +64 -0
- yeaboi/prompts/sprint_planner.py +242 -0
- yeaboi/prompts/standup.py +76 -0
- yeaboi/prompts/story_writer.py +317 -0
- yeaboi/prompts/system.py +107 -0
- yeaboi/prompts/task_decomposer.py +211 -0
- yeaboi/questionnaire_io.py +196 -0
- yeaboi/repl/__init__.py +1316 -0
- yeaboi/repl/_intake_menu.py +93 -0
- yeaboi/repl/_io.py +573 -0
- yeaboi/repl/_mode_menu.py +66 -0
- yeaboi/repl/_questionnaire.py +396 -0
- yeaboi/repl/_review.py +162 -0
- yeaboi/repl/_ui.py +177 -0
- yeaboi/reporting/__init__.py +55 -0
- yeaboi/reporting/activity.py +165 -0
- yeaboi/reporting/engine.py +375 -0
- yeaboi/reporting/export.py +192 -0
- yeaboi/reporting/presentation.py +285 -0
- yeaboi/reporting/render.py +114 -0
- yeaboi/reporting/sprints.py +163 -0
- yeaboi/reporting/store.py +196 -0
- yeaboi/retro/__init__.py +29 -0
- yeaboi/retro/board.py +428 -0
- yeaboi/retro/engine.py +112 -0
- yeaboi/retro/export.py +164 -0
- yeaboi/retro/page.py +856 -0
- yeaboi/retro/server.py +392 -0
- yeaboi/retro/store.py +212 -0
- yeaboi/retro/tunnel.py +229 -0
- yeaboi/sessions.py +767 -0
- yeaboi/setup_wizard.py +288 -0
- yeaboi/skills/__init__.py +0 -0
- yeaboi/skills/scrum-planner/README.md +159 -0
- yeaboi/skills/scrum-planner/SKILL.md +338 -0
- yeaboi/skills/scrum-planner/references/cli-and-generation.md +85 -0
- yeaboi/skills/scrum-planner/references/output-and-review.md +125 -0
- yeaboi/skills/scrum-planner/scripts/canvas.py +233 -0
- yeaboi/skills/scrum-planner/scripts/canvas_push.py +231 -0
- yeaboi/standup/__init__.py +26 -0
- yeaboi/standup/collector.py +203 -0
- yeaboi/standup/confidence.py +171 -0
- yeaboi/standup/delivery.py +229 -0
- yeaboi/standup/engine.py +358 -0
- yeaboi/standup/errors.py +26 -0
- yeaboi/standup/export.py +201 -0
- yeaboi/standup/interactive.py +119 -0
- yeaboi/standup/render.py +125 -0
- yeaboi/standup/scheduler.py +297 -0
- yeaboi/standup/sprint_context.py +104 -0
- yeaboi/standup/store.py +356 -0
- yeaboi/team_profile.py +687 -0
- yeaboi/team_profile_exporter.py +2237 -0
- yeaboi/telemetry.py +256 -0
- yeaboi/tools/__init__.py +135 -0
- yeaboi/tools/azure_devops.py +1015 -0
- yeaboi/tools/calendar_tools.py +308 -0
- yeaboi/tools/codebase.py +559 -0
- yeaboi/tools/confluence.py +439 -0
- yeaboi/tools/github.py +434 -0
- yeaboi/tools/jira.py +835 -0
- yeaboi/tools/llm_tools.py +158 -0
- yeaboi/tools/local_git.py +83 -0
- yeaboi/tools/notion.py +484 -0
- yeaboi/tools/team_learning.py +5421 -0
- yeaboi/ui/__init__.py +2 -0
- yeaboi/ui/mode_select/__init__.py +4967 -0
- yeaboi/ui/mode_select/screens/__init__.py +1 -0
- yeaboi/ui/mode_select/screens/_project_cards.py +588 -0
- yeaboi/ui/mode_select/screens/_project_list_screen.py +770 -0
- yeaboi/ui/mode_select/screens/_screens.py +381 -0
- yeaboi/ui/mode_select/screens/_screens_secondary.py +4373 -0
- yeaboi/ui/provider_select/__init__.py +893 -0
- yeaboi/ui/provider_select/_config.py +41 -0
- yeaboi/ui/provider_select/_constants.py +237 -0
- yeaboi/ui/provider_select/_nav.py +63 -0
- yeaboi/ui/provider_select/_phase_confluence.py +261 -0
- yeaboi/ui/provider_select/_phase_issue_tracking.py +387 -0
- yeaboi/ui/provider_select/_phase_notion.py +241 -0
- yeaboi/ui/provider_select/_transitions.py +95 -0
- yeaboi/ui/provider_select/_verification.py +508 -0
- yeaboi/ui/provider_select/screens/__init__.py +1 -0
- yeaboi/ui/provider_select/screens/_screens.py +565 -0
- yeaboi/ui/provider_select/screens/_screens_vc.py +378 -0
- yeaboi/ui/session/__init__.py +389 -0
- yeaboi/ui/session/_dry_run.py +98 -0
- yeaboi/ui/session/_renderers.py +1179 -0
- yeaboi/ui/session/_utils.py +394 -0
- yeaboi/ui/session/editor/__init__.py +1 -0
- yeaboi/ui/session/editor/_editor.py +717 -0
- yeaboi/ui/session/editor/_editor_artifacts.py +529 -0
- yeaboi/ui/session/editor/_editor_core.py +457 -0
- yeaboi/ui/session/phases/__init__.py +1 -0
- yeaboi/ui/session/phases/_phases.py +1685 -0
- yeaboi/ui/session/phases/_phases_intake.py +659 -0
- yeaboi/ui/session/phases/_phases_review.py +323 -0
- yeaboi/ui/session/screens/__init__.py +1 -0
- yeaboi/ui/session/screens/_accordion.py +508 -0
- yeaboi/ui/session/screens/_screens.py +209 -0
- yeaboi/ui/session/screens/_screens_input.py +386 -0
- yeaboi/ui/session/screens/_screens_pipeline.py +543 -0
- yeaboi/ui/shared/__init__.py +43 -0
- yeaboi/ui/shared/_animations.py +164 -0
- yeaboi/ui/shared/_ascii_font.py +67 -0
- yeaboi/ui/shared/_components.py +378 -0
- yeaboi/ui/shared/_input.py +313 -0
- yeaboi/ui/shared/_music_bar.py +159 -0
- yeaboi/ui/shared/_tips.py +107 -0
- yeaboi/ui/shared/_voice_input.py +207 -0
- yeaboi/ui/shared/_wordmarks.py +150 -0
- yeaboi/ui/splash.py +331 -0
- yeaboi/voice.py +196 -0
- yeaboi-2.8.0.dist-info/METADATA +2344 -0
- yeaboi-2.8.0.dist-info/RECORD +153 -0
- yeaboi-2.8.0.dist-info/WHEEL +4 -0
- yeaboi-2.8.0.dist-info/entry_points.txt +3 -0
- 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"
|
yeaboi/agent/__init__.py
ADDED
|
@@ -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
|