Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ As usage grows, the platform needs stronger derived data pipelines, performance
## Real-Time Engineer Experience

- [x] MCP sidecar with on-demand stats, friction reports, and recommendations
- [ ] [P0] Proactive coaching skill that activates at session start with contextual suggestions
- [x] [P0] Proactive coaching skill that activates at session start with contextual suggestions
- [ ] [P0] Live session signals that stream friction, satisfaction, and risk as work happens
- [ ] [P1] In-session workflow nudges based on project playbooks and prior failures
- [ ] [P1] Daily and weekly personal recaps inside the sidecar
Expand Down
2 changes: 2 additions & 0 deletions src/primer/common/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2216,6 +2216,8 @@ class CoachingBrief(BaseModel):
sections: list[CoachingSection]
sessions_analyzed: int = 0
generated_at: str
brief_type: Literal["retrospective", "session_start"] = "retrospective"
context_summary: str | None = None


# --- Narrative Insights ---
Expand Down
21 changes: 21 additions & 0 deletions src/primer/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
primer_friction_report,
primer_my_stats,
primer_recommendations,
primer_session_start_coaching,
primer_sync,
primer_team_overview,
)
Expand Down Expand Up @@ -72,5 +73,25 @@ def coaching(days: int = 30) -> str:
return primer_coaching(days=days)


@mcp.tool()
def session_start_coaching(
project_name: str | None = None,
workflow_hint: str | None = None,
task_hint: str | None = None,
days: int = 90,
) -> str:
"""Get a contextual session-start brief before you begin work.

Uses project context, workflow hints, and peer-backed evidence to suggest
a strong starting pattern, tool/model choices, and likely pitfalls.
"""
return primer_session_start_coaching(
project_name=project_name,
workflow_hint=workflow_hint,
task_hint=task_hint,
days=days,
)


if __name__ == "__main__":
mcp.run()
66 changes: 57 additions & 9 deletions src/primer/mcp/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ def _admin_headers() -> dict:
return {"x-admin-key": admin_key, "x-api-key": API_KEY}


def _render_coaching_brief(data: dict) -> str:
brief_type = data.get("brief_type", "retrospective")
heading = (
"## Your Primer Session-Start Brief\n"
if brief_type == "session_start"
else "## Your Primer Coaching Brief\n"
)
lines = [heading]
lines.append(f"**Status**: {data['status_summary']}\n")
if data.get("context_summary"):
lines.append(f"**Context**: {data['context_summary']}\n")
for section in data.get("sections", []):
lines.append(f"### {section['title']}")
for item in section.get("items", []):
lines.append(f"- {item}")
lines.append("")
return "\n".join(lines)


def primer_sync() -> str:
"""Sync local session history to the Primer server (backfill missing sessions)."""
if not API_KEY:
Expand Down Expand Up @@ -118,15 +137,44 @@ def primer_coaching(days: int = 30) -> str:
timeout=30,
)
if resp.status_code == 200:
data = resp.json()
lines = ["## Your Primer Coaching Brief\n"]
lines.append(f"**Status**: {data['status_summary']}\n")
for section in data.get("sections", []):
lines.append(f"### {section['title']}")
for item in section.get("items", []):
lines.append(f"- {item}")
lines.append("")
return "\n".join(lines)
return _render_coaching_brief(resp.json())
return f"Error: {resp.status_code} - {resp.text}"
except httpx.RequestError as e:
return f"Error connecting to server: {e}"


def primer_session_start_coaching(
project_name: str | None = None,
workflow_hint: str | None = None,
task_hint: str | None = None,
days: int = 90,
) -> str:
"""Get a contextual coaching brief right before starting a session.

Uses project context, workflow hints, and prior team evidence
to suggest a strong opening pattern, tool/model choices, and pitfalls.
"""
if not API_KEY:
return "Error: PRIMER_API_KEY not set"
try:
params = {
key: value
for key, value in {
"project_name": project_name,
"workflow_hint": workflow_hint,
"task_hint": task_hint,
"days": days,
}.items()
if value is not None
}
resp = httpx.get(
f"{SERVER_URL}/api/v1/analytics/coaching/session-start",
params=params,
headers={"x-api-key": API_KEY},
timeout=30,
)
if resp.status_code == 200:
return _render_coaching_brief(resp.json())
return f"Error: {resp.status_code} - {resp.text}"
except httpx.RequestError as e:
return f"Error connecting to server: {e}"
31 changes: 31 additions & 0 deletions src/primer/server/routers/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,37 @@ def coaching_brief(
return get_coaching_brief(db, engineer_id=engineer_id, team_id=team_id, days=days)


@router.get("/coaching/session-start", response_model=CoachingBrief)
def coaching_session_start(
days: int = Query(default=90, ge=1, le=365),
project_name: str | None = None,
workflow_hint: str | None = None,
task_hint: str | None = None,
db: Session = Depends(get_db),
auth: AuthContext = Depends(get_auth_context),
):
from primer.server.services.coaching_service import get_session_start_brief

engineer_id = auth.engineer_id
team_id = auth.team_id
if auth.role == "admin":
raise HTTPException(
status_code=400,
detail=(
"Session-start coaching requires an engineer context. Use an engineer API key."
),
)
return get_session_start_brief(
db,
engineer_id=engineer_id,
team_id=team_id,
days=days,
project_name=project_name,
workflow_hint=workflow_hint,
task_hint=task_hint,
)


@router.get("/narrative/status", response_model=NarrativeStatusResponse)
def narrative_status(
auth: AuthContext = Depends(get_auth_context),
Expand Down
Loading
Loading